[Haskell-cafe] Speed of Error handling with Continuations vs.
Eithers
Max Cantor
mxcantor at gmail.com
Mon May 10 05:38:49 EDT 2010
Based on some discussions in #haskell, it seemed to be a consensus that using a modified continuation monad for Error handling instead of Eithers would be a significant optimization since it would eliminate a lot of conditional branching (everytime >>= is called in the Either monad, there is a conditional.
I implemented a ErrCPS monad which does exactly that, but the speed has been disappointing. It runs almost exactly 3x slower than a drop in replacement using the MonadError instance of Either from mtl.
mkEMA and midError are basically toy functions but I dont know why Either is so much faster. I've experimented with putting some seq's in the bindErrCPS and even {-# INLINE (>>=) #-} in the Monad instance, but to no avail.
I've copy/pasted the code below, any suggestions on optimization, or if this is simply a bad idea would be much appreciated. Strangely, compiling with -O2 seems to have no effect on the speed:
-Max
> {-# LANGUAGE MultiParamTypeClasses #-}
> {-# LANGUAGE FlexibleInstances #-}
> {-# LANGUAGE FlexibleContexts #-}
> {-# LANGUAGE Rank2Types #-}
> module Main where
>
> import Control.Applicative
> import Control.Monad.Error -- hiding (foldM)
> import Control.Monad.Trans
> import Control.Monad hiding (foldM)
> import System.Random
> import Control.Monad.Identity (runIdentity, Identity)
> import Control.Monad.Reader.Class
> import Data.Time.LocalTime as Time -- for benchmarking
> import Data.Time.Calendar (Day)
> import Data.Time.LocalTime (getZonedTime)
> midError :: MonadError String m => Double -> Double -> m Double
> midError a b = if (b < 1) then throwError "check val"
> else let r = (a + b) / 2 in r `seq` (return r)
> mkEMA l = foldM midError 1 l
> newtype ErrCPS e m a = ErrCPS { runErrCPS :: forall r . (e -> m r) -- error handler
> -> (a -> m r) -- success handler
> -> m r }
>
> {-# INLINE retErrCPS #-}
> retErrCPS :: a -> ErrCPS e m a
> retErrCPS x = ErrCPS $ \_ good -> good x
>
> {-# INLINE bindErrCPS #-}
> bindErrCPS :: ErrCPS e m b -> (b -> ErrCPS e m a) -> ErrCPS e m a
> bindErrCPS m f = ErrCPS $ \err good -> runErrCPS m err $ \x -> runErrCPS (f x) err good
>
> instance Monad m => Monad (ErrCPS e m) where
> return = retErrCPS
> (>>=) = bindErrCPS
> main :: IO ()
> main = do
> let n = 500000
> runEither e b g = either b g e
> runTest f s = do
> sg <- newStdGen
> let l = take n $ randomRs (2, 50000) sg
> mapM_ (\e -> e `seq` return ()) l
> stopwatch $ f (mkEMA l)
> (putStr . show)
> (putStr . (s ++) . show)
>
> forever $ do runTest runEither "either: "
> runTest runErrCPS "errCPS: "
ErrCPS based code seems to run almost exactly 3x slower than the
Either based code:
errCPS: 37453.226 Action ran in: 30 msec
either: 26803.055 Action ran in: 11 msec
errCPS: 15840.626 Action ran in: 34 msec
either: 32556.881 Action ran in: 10 msec
errCPS: 38933.121 Action ran in: 30 msec
either: 35370.820 Action ran in: 11 msec
...
>
> instance (Error e, Monad m) => MonadError e (ErrCPS e m) where
> throwError = errCPS
> catchError m f = ErrCPS $ \err good -> runErrCPS m (\e -> runErrCPS (f e) err good) good
>
>
> -- * MTL stuff
> instance MonadTrans (ErrCPS e ) where lift m = ErrCPS $ \_ good -> m >>= good
> instance (MonadIO m) => MonadIO (ErrCPS e m ) where liftIO = lift . liftIO
>
Random utility stuff
> stopwatch :: IO () -> IO ()
> stopwatch act = do
> t1 <- getFastTimeOfDay
> act
> t2 <- getFastTimeOfDay
> putStrLn $ " Action ran in: " ++ show (t2 - t1) ++ " msec"
> type FastTimeOfDay = Int
>
> -- | Return the current trading day. This should respect the
> -- fact that the Trading Day ranges from
> -- SingTime 6am (UTC -02:00) to SST 5:59 am (UTC -1:59).
> getTradingDay :: IO Day
> getTradingDay = error "getTradingDay undefined"
>
> getFastTimeOfDay :: IO FastTimeOfDay
> getFastTimeOfDay = getZonedTime >>=
> (return . fastFromTimeOfDay . Time.localTimeOfDay . Time.zonedTimeToLocalTime)
>
> timeOfDayFromFast :: FastTimeOfDay -> Time.TimeOfDay
> timeOfDayFromFast fast = Time.TimeOfDay
> { Time.todHour = fromIntegral (fast `div` (3600 * 1000))
> , Time.todMin = fromIntegral (fast `div` (60 * 1000)) `mod` 60
> , Time.todSec = fromRational $ (fromIntegral fast) / 1000
> }
>
> fastFromTimeOfDay :: Time.TimeOfDay -> FastTimeOfDay
> fastFromTimeOfDay t = fromIntegral $
> ((Time.todHour t) * 3600000) +
> ((Time.todMin t) * 60000) +
> (round $ 1000 * Time.todSec t)
>
> instance (Monad m) => Functor (ErrCPS e m) where
> fmap f m = ErrCPS $ \err good -> runErrCPS m err (good . f)
>
> instance (Monad m) => Applicative (ErrCPS e m) where
> pure = return
> f <*> a = do f' <- f
> a' <- a
> return $ f' a'
>
> errCPS :: forall e m a . e -> ErrCPS e m a
> errCPS e = ErrCPS $ \err _ -> err e
>
>
More information about the Haskell-Cafe
mailing list