Speed of Error handling with Continuations vs. Eithers

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

On Mon, May 10, 2010 at 5:38 AM, Max Cantor
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.
My suspicion, based on using a similar monad to implement IO in Eager Haskell, is that you're creating a lot of closures. This is rather more expensive in general than the extra control flow required to inspect the Eithers. In more detail: CPS works well if the compiler can inline most of the continuation passing and turn your code back into direct style, at least along the "no failures" path. In this case you can avoid creating closures except at what would have been actual function call points in your original code, and at catch points for the error continuation. However, I expect that you're probably calling functions that are polymorphic in Monad (stuff like mapM etc.) that is not being inlined or specialized. These end up building a continuation rather naively on the heap. You're essentially moving the call stack to the heap, and the compiler can't assist you in moving it back again; hence, slow code. To make matters worse, you get a lot more branch prediction leverage with pointer-tagged Either than you possibly could with a closure invocation on a modern architecture. But I suspect that's rather unimportant compared to allocation time / memory footprint issues here. -Jan-Willem Maessen
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 [... code snipped]

Makes sense. From what you wrote, it seems like this might be a dead-end and can't really be optimized away. Do you agree? Max On May 10, 2010, at 8:38 PM, Jan-Willem Maessen wrote:
On Mon, May 10, 2010 at 5:38 AM, Max Cantor
wrote: 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. My suspicion, based on using a similar monad to implement IO in Eager Haskell, is that you're creating a lot of closures. This is rather more expensive in general than the extra control flow required to inspect the Eithers.
In more detail: CPS works well if the compiler can inline most of the continuation passing and turn your code back into direct style, at least along the "no failures" path. In this case you can avoid creating closures except at what would have been actual function call points in your original code, and at catch points for the error continuation. However, I expect that you're probably calling functions that are polymorphic in Monad (stuff like mapM etc.) that is not being inlined or specialized. These end up building a continuation rather naively on the heap. You're essentially moving the call stack to the heap, and the compiler can't assist you in moving it back again; hence, slow code.
To make matters worse, you get a lot more branch prediction leverage with pointer-tagged Either than you possibly could with a closure invocation on a modern architecture. But I suspect that's rather unimportant compared to allocation time / memory footprint issues here.
-Jan-Willem Maessen
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 [... code snipped]

Max Cantor wrote:
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.
I have noticed speedup in my CPS version of Maybe[1] (kidnapped from the Wiki) so the difference is curious. Jan-Willem's comments about closures are significant when doing CPS work, but I'd expect Maybe and Either to perform similarly, whatever their performance is. It's been a while since I've benchmarked MaybeCPS, so perhaps I now have the slowdown too. Let's look at the code and see if we can find other differences... [1] http://community.haskell.org/~wren/wren-extras/src/Control/Monad/MaybeCPS.hs Here's one big difference:
newtype ErrCPS e m a = ErrCPS { runErrCPS :: forall r . (e -> m r) -- error handler -> (a -> m r) -- success handler -> m r }
The analogous version I use is: newtype MaybeCPS a = MaybeCPS (forall r. (a -> Maybe r) -> Maybe r) While I also offer a transformer version of MaybeCPS, the transformer *does* suffer from significant slowdown. Also, for MaybeCPS it's better to leave the handlers inline in client code rather than to abstract them out; that helps to keep things concrete. So perhaps you should first try a direct CPS translation: newtype ErrCPS e a = ErrCPS (forall r. (a -> Either e r) -> Either e r) runErrCPS :: ErrCPS e a -> Either e a runErrCPS (ErrCPS f) = f return I'd be curious if this version suffers the same slowdown. -- Live well, ~wren

wren ng thornton wrote:
Here's one big difference:
newtype ErrCPS e m a = ErrCPS { runErrCPS :: forall r . (e -> m r) -- error handler -> (a -> m r) -- success handler -> m r }
The analogous version I use is:
newtype MaybeCPS a = MaybeCPS (forall r. (a -> Maybe r) -> Maybe r)
While I also offer a transformer version of MaybeCPS, the transformer *does* suffer from significant slowdown. Also, for MaybeCPS it's better to leave the handlers inline in client code rather than to abstract them out; that helps to keep things concrete. So perhaps you should first try a direct CPS translation:
newtype ErrCPS e a = ErrCPS (forall r. (a -> Either e r) -> Either e r)
runErrCPS :: ErrCPS e a -> Either e a runErrCPS (ErrCPS f) = f return
I'd be curious if this version suffers the same slowdown.
With this change [1] I can't notice any difference for your benchmark[2]. Then again, all the runTest calls take 0 msec and I've had no luck making the computation take much time; perhaps your computer can detect a difference. You may want to see what standard benchmarking tools like Microbench[3] or the magnificent Criterion[4] have to say. I'd do it myself, but I haven't had a chance to reinstall everything since getting my new computer (due to the installation issues on newer versions of OSX). [1] http://community.haskell.org/~wren/wren-extras/src/Control/Monad/ErrCPS.hs [2] http://community.haskell.org/~wren/wren-extras/test/Control/Monad/ErrCPS/Max... [3] http://hackage.haskell.org/package/microbench [4] http://hackage.haskell.org/package/criterion -- Live well, ~wren

On Wed, May 12, 2010 at 7:50 AM, wren ng thornton
wren ng thornton wrote:
Here's one big difference:
newtype ErrCPS e m a = ErrCPS { runErrCPS :: forall r . (e -> m r) -- error handler -> (a -> m r) -- success handler -> m r }
The analogous version I use is:
newtype MaybeCPS a = MaybeCPS (forall r. (a -> Maybe r) -> Maybe r)
While I also offer a transformer version of MaybeCPS, the transformer *does* suffer from significant slowdown. Also, for MaybeCPS it's better to leave the handlers inline in client code rather than to abstract them out; that helps to keep things concrete. So perhaps you should first try a direct CPS translation:
newtype ErrCPS e a = ErrCPS (forall r. (a -> Either e r) -> Either e r)
runErrCPS :: ErrCPS e a -> Either e a runErrCPS (ErrCPS f) = f return
I'd be curious if this version suffers the same slowdown.
With this change [1] I can't notice any difference for your benchmark[2]. Then again, all the runTest calls take 0 msec and I've had no luck making the computation take much time; perhaps your computer can detect a difference.
On my machine, with ghc-6.12.1, yours and the original ErrCPS give quite similar results, both ~2x slower than Either. However it's important to note that these results are highly dependent on the monadic expressions being evaluated, with a different benchmark you can get an huge speedup with the CPS versions. mkEMA is in fact quite peculiar, since there's no catchError and the throwError call is rarely (or never?) made, and thanks to foldM you get that (>>=) is only used in a right associated way, which is the ideal situation for Either. In a larger program one might mix the two to get the best of both worlds i guess, and maybe we can make a library where each combinator from Control.Monad is reimplemented with the most fitting alternative behind the scenes. the nice part is that you can get the CPS version in a generic way using Codensity: http://hackage.haskell.org/packages/archive/mmtl/0.1/doc/html/Control-Monad-...
You may want to see what standard benchmarking tools like Microbench[3] or the magnificent Criterion[4] have to say. I'd do it myself, but I haven't had a chance to reinstall everything since getting my new computer (due to the installation issues on newer versions of OSX).
[1] http://community.haskell.org/~wren/wren-extras/src/Control/Monad/ErrCPS.hs
[2] http://community.haskell.org/~wren/wren-extras/test/Control/Monad/ErrCPS/Max...
[3] http://hackage.haskell.org/package/microbench
[4] http://hackage.haskell.org/package/criterion
-- Live well, ~wren _______________________________________________ Haskell-Cafe mailing list Haskell-Cafe@haskell.org http://www.haskell.org/mailman/listinfo/haskell-cafe

Andrea Vezzosi wrote:
wren ng thornton wrote:
With this change [1] I can't notice any difference for your benchmark[2]. Then again, all the runTest calls take 0 msec and I've had no luck making the computation take much time; perhaps your computer can detect a difference.
On my machine, with ghc-6.12.1, yours and the original ErrCPS give quite similar results, both ~2x slower than Either. However it's important to note that these results are highly dependent on the monadic expressions being evaluated, with a different benchmark you can get an huge speedup with the CPS versions.
That's very curious. After installing Criterion, my machine (OSX 10.5.8 2.8GHz Intel Core2Duo, GHC 6.12.1 with -O2) shows only 1% difference between my ErrCPS and Either on this benchmark. Alas, I can't print kernel density graphs since Crieterion charts are broken on 6.12. It seems buggy that your platform would behave so much differently...
mkEMA is in fact quite peculiar, since there's no catchError and the throwError call is rarely (or never?) made, and thanks to foldM you get that (>>=) is only used in a right associated way, which is the ideal situation for Either.
Indeed, mkEMA is a sort of worst-case comparison that doesn't take advantage of the ability to short-circuit. -- Live well, ~wren

On Thu, May 13, 2010 at 10:51 AM, wren ng thornton
Andrea Vezzosi wrote:
wren ng thornton wrote:
With this change [1] I can't notice any difference for your benchmark[2]. Then again, all the runTest calls take 0 msec and I've had no luck making the computation take much time; perhaps your computer can detect a difference.
On my machine, with ghc-6.12.1, yours and the original ErrCPS give quite similar results, both ~2x slower than Either. However it's important to note that these results are highly dependent on the monadic expressions being evaluated, with a different benchmark you can get an huge speedup with the CPS versions.
That's very curious. After installing Criterion, my machine (OSX 10.5.8 2.8GHz Intel Core2Duo, GHC 6.12.1 with -O2) shows only 1% difference between my ErrCPS and Either on this benchmark. Alas, I can't print kernel density graphs since Crieterion charts are broken on 6.12. It seems buggy that your platform would behave so much differently...
I got the measurements from the original code, could you share the code that uses criterion instead?
mkEMA is in fact quite peculiar, since there's no catchError and the throwError call is rarely (or never?) made, and thanks to foldM you get that (>>=) is only used in a right associated way, which is the ideal situation for Either.
Indeed, mkEMA is a sort of worst-case comparison that doesn't take advantage of the ability to short-circuit.
-- Live well, ~wren _______________________________________________ Haskell-Cafe mailing list Haskell-Cafe@haskell.org http://www.haskell.org/mailman/listinfo/haskell-cafe

Andrea Vezzosi wrote:
On Thu, May 13, 2010 at 10:51 AM, wren ng thornton
wrote: Andrea Vezzosi wrote:
wren ng thornton wrote:
With this change [1] I can't notice any difference for your benchmark[2]. Then again, all the runTest calls take 0 msec and I've had no luck making the computation take much time; perhaps your computer can detect a difference. On my machine, with ghc-6.12.1, yours and the original ErrCPS give quite similar results, both ~2x slower than Either. However it's important to note that these results are highly dependent on the monadic expressions being evaluated, with a different benchmark you can get an huge speedup with the CPS versions.
That's very curious. After installing Criterion, my machine (OSX 10.5.8 2.8GHz Intel Core2Duo, GHC 6.12.1 with -O2) shows only 1% difference between my ErrCPS and Either on this benchmark. Alas, I can't print kernel density graphs since Crieterion charts are broken on 6.12. It seems buggy that your platform would behave so much differently...
I got the measurements from the original code, could you share the code that uses criterion instead?
The 1% number was buggy because I hadn't factored the generation of random lists out of the benchmark. But, having fixed that, I still can't replicate your numbers: I get 12us for Either, vs 17us for EitherCPS. http://community.haskell.org/~wren/wren-extras/test/Control/Monad/ErrCPS/Cri... Yet another version of the same benchmark, this time using Microbench: http://community.haskell.org/~wren/wren-extras/test/Control/Monad/ErrCPS/Mic... Microbench seems to replicate your numbers better: 2551.930ns vs 4466.832ns (or 391.86 vs 223.87 calls per second)--- though this is getting into the range where there might be Int overflow issues corrupting the results (a similar problem showed up when benchmarking Data.Trie vs Data.Map), so it may warrant further investigation. -- Live well, ~wren

On Thu, May 13, 2010 at 8:13 PM, wren ng thornton
Andrea Vezzosi wrote:
On Thu, May 13, 2010 at 10:51 AM, wren ng thornton
wrote: Andrea Vezzosi wrote:
wren ng thornton wrote:
With this change [1] I can't notice any difference for your benchmark[2]. Then again, all the runTest calls take 0 msec and I've had no luck making the computation take much time; perhaps your computer can detect a difference.
On my machine, with ghc-6.12.1, yours and the original ErrCPS give quite similar results, both ~2x slower than Either. However it's important to note that these results are highly dependent on the monadic expressions being evaluated, with a different benchmark you can get an huge speedup with the CPS versions.
That's very curious. After installing Criterion, my machine (OSX 10.5.8 2.8GHz Intel Core2Duo, GHC 6.12.1 with -O2) shows only 1% difference between my ErrCPS and Either on this benchmark. Alas, I can't print kernel density graphs since Crieterion charts are broken on 6.12. It seems buggy that your platform would behave so much differently...
I got the measurements from the original code, could you share the code that uses criterion instead?
The 1% number was buggy because I hadn't factored the generation of random lists out of the benchmark. But, having fixed that, I still can't replicate your numbers: I get 12us for Either, vs 17us for EitherCPS.
http://community.haskell.org/~wren/wren-extras/test/Control/Monad/ErrCPS/Cri...
Yet another version of the same benchmark, this time using Microbench:
http://community.haskell.org/~wren/wren-extras/test/Control/Monad/ErrCPS/Mic...
Microbench seems to replicate your numbers better: 2551.930ns vs 4466.832ns (or 391.86 vs 223.87 calls per second)--- though this is getting into the range where there might be Int overflow issues corrupting the results (a similar problem showed up when benchmarking Data.Trie vs Data.Map), so it may warrant further investigation.
That might be the case, i'm on 64bit: saizan@astarte:~$ uname -a Linux astarte 2.6.32-ARCH #1 SMP PREEMPT Tue Feb 23 19:43:46 CET 2010 x86_64 Intel(R) Core(TM)2 Duo CPU E8400 @ 3.00GHz GenuineIntel GNU/Linux saizan@astarte:~$ ./CriterionBenchmark warming up estimating clock resolution... mean is 6.834442 us (80001 iterations) found 1240 outliers among 79998 samples (1.6%) 1131 (1.4%) high severe estimating cost of a clock call... mean is 107.2316 ns (41 iterations) benchmarking Either collecting 100 samples, 1039 iterations each, in estimated 683.8220 ms bootstrapping with 100000 resamples mean: 6.563462 us, lb 6.553649 us, ub 6.570454 us, ci 0.950 std dev: 41.74602 ns, lb 23.76971 ns, ub 67.67842 ns, ci 0.950 found 8 outliers among 100 samples (8.0%) 2 (2.0%) low severe 4 (4.0%) high mild 2 (2.0%) high severe variance introduced by outliers: 0.990% variance is unaffected by outliers benchmarking ErrCPS collecting 100 samples, 1 iterations each, in estimated 1.334000 s bootstrapping with 100000 resamples mean: 13.14468 ms, lb 13.10442 ms, ub 13.18208 ms, ci 0.950 std dev: 198.3150 us, lb 182.0600 us, ub 220.7957 us, ci 0.950 variance introduced by outliers: 0.993% variance is unaffected by outliers If i'm reading it correctly this gives even worse results: 6us vs. 13ms
-- Live well, ~wren _______________________________________________ Haskell-Cafe mailing list Haskell-Cafe@haskell.org http://www.haskell.org/mailman/listinfo/haskell-cafe

On Tue, May 11, 2010 at 8:28 PM, wren ng thornton
Max Cantor wrote:
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.
I have noticed speedup in my CPS version of Maybe[1] (kidnapped from the Wiki) so the difference is curious. Jan-Willem's comments about closures are significant when doing CPS work, but I'd expect Maybe and Either to perform similarly, whatever their performance is. It's been a while since I've benchmarked MaybeCPS, so perhaps I now have the slowdown too. Let's look at the code and see if we can find other differences...
[1] http://community.haskell.org/~wren/wren-extras/src/Control/Monad/MaybeCPS.hs
Here's one big difference:
newtype ErrCPS e m a = ErrCPS { runErrCPS :: forall r . (e -> m r) -- error handler -> (a -> m r) -- success handler -> m r }
The analogous version I use is:
newtype MaybeCPS a = MaybeCPS (forall r. (a -> Maybe r) -> Maybe r)
While I also offer a transformer version of MaybeCPS, the transformer *does* suffer from significant slowdown. Also, for MaybeCPS it's better to leave the handlers inline in client code rather than to abstract them out; that helps to keep things concrete. So perhaps you should first try a direct CPS translation:
Is the CPS transformed MaybeT slower because it's done in 2-CPS, rather than in 1-CPS like the MaybeCPS? I only did MaybeT in 2-CPS because it was the easiest, not because I thought it would be easiest. Antoine

Antoine Latter wrote:
While I also offer a transformer version of MaybeCPS, the transformer *does* suffer from significant slowdown. Also, for MaybeCPS it's better to leave the handlers inline in client code rather than to abstract them out; that helps to keep things concrete. So perhaps you should first try a direct CPS translation:
Is the CPS transformed MaybeT slower because it's done in 2-CPS, rather than in 1-CPS like the MaybeCPS? I only did MaybeT in 2-CPS because it was the easiest, not because I thought it would be easiest.
I'm not sure how much of it is due to the 2-CPS vs how much is due to the loss of concrete case-analysis and tail-calls in crucial areas. As I noted in comments on the non-transformer version, there are a number of subtle issues such as the choice between let-binding and case analysis which have major effects on performance, so it's tricky to make a MaybeCPS which doesn't impose a performance overhead. A big part of the problem is that once you move to the transformer version, you can't just jump to the next handler--- you also need to carry around whatever the other monad would add to Nothing. Once you move to 2-CPS, the representation is similar enough to LogicT (==ListCPST) that you seem to loose the benefits of Maybe over []. -- Live well, ~wren

On Mon, 10 May 2010, Max Cantor wrote:
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 assumed that GHC also has optimizations for conditional branching.

You did it wrong. All you did was Church encode the Either type.
Your bind is still doing a case-analysis. All you have to do is use
ContT r (Either e). The bind implementation for ContT is completely
independent of the underlying monad. It doesn't even require the m in
ContT r m to be a functor, let alone a monad. Therefore the ContT
bind doesn't do any case-analysis because it doesn't know anything
about the underlying monad. One way to look at what is happening is
to compare it to Andrzej Filiniski's work in "Representing Monads" and
"Representing Layered Monads".
On Mon, May 10, 2010 at 4:38 AM, Max Cantor
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
_______________________________________________ Haskell-Cafe mailing list Haskell-Cafe@haskell.org http://www.haskell.org/mailman/listinfo/haskell-cafe

On Fri, May 14, 2010 at 4:25 PM, Derek Elkins
You did it wrong. All you did was Church encode the Either type. Your bind is still doing a case-analysis. All you have to do is use ContT r (Either e). The bind implementation for ContT is completely independent of the underlying monad. It doesn't even require the m in ContT r m to be a functor, let alone a monad. Therefore the ContT bind doesn't do any case-analysis because it doesn't know anything about the underlying monad. One way to look at what is happening is to compare it to Andrzej Filiniski's work in "Representing Monads" and "Representing Layered Monads".
Would you then use callCC to get the required short-circuit-on-error behavior? A church encoding of Either coded as a monad transformer still wouldn't hit the inner monad on bind, even if it is weaving the left and right continuations together. Antoine

On Fri, May 14, 2010 at 4:53 PM, Antoine Latter
On Fri, May 14, 2010 at 4:25 PM, Derek Elkins
wrote: You did it wrong. All you did was Church encode the Either type. Your bind is still doing a case-analysis. All you have to do is use ContT r (Either e). The bind implementation for ContT is completely independent of the underlying monad. It doesn't even require the m in ContT r m to be a functor, let alone a monad. Therefore the ContT bind doesn't do any case-analysis because it doesn't know anything about the underlying monad. One way to look at what is happening is to compare it to Andrzej Filiniski's work in "Representing Monads" and "Representing Layered Monads".
Would you then use callCC to get the required short-circuit-on-error behavior?
A church encoding of Either coded as a monad transformer still wouldn't hit the inner monad on bind, even if it is weaving the left and right continuations together.
callCC wouldn't work well here. What would work better is another control operator commonly called 'control' which does not resume if the passed in continuation isn't invoked. However, it's usually even clearer (or at least more concise) in these situations to work with the continuation passing style directly. -- fail directly using CPS fail :: String -> ContT r (Either String) a fail s = ContT $ \k -> Left s

Where is my bind statement doing a case analysis? Isn't it just propagating, in a sense, the case analysis that came from values coming into the monad via return or via throwError? Also, why wouldn't callCC work here? I'm not that familiar with the ContT monad so any more details would be very much appreciated. Max On May 15, 2010, at 6:40 AM, Derek Elkins wrote:
On Fri, May 14, 2010 at 4:53 PM, Antoine Latter
wrote: On Fri, May 14, 2010 at 4:25 PM, Derek Elkins
wrote: You did it wrong. All you did was Church encode the Either type. Your bind is still doing a case-analysis. All you have to do is use ContT r (Either e). The bind implementation for ContT is completely independent of the underlying monad. It doesn't even require the m in ContT r m to be a functor, let alone a monad. Therefore the ContT bind doesn't do any case-analysis because it doesn't know anything about the underlying monad. One way to look at what is happening is to compare it to Andrzej Filiniski's work in "Representing Monads" and "Representing Layered Monads".
Would you then use callCC to get the required short-circuit-on-error behavior?
A church encoding of Either coded as a monad transformer still wouldn't hit the inner monad on bind, even if it is weaving the left and right continuations together.
callCC wouldn't work well here. What would work better is another control operator commonly called 'control' which does not resume if the passed in continuation isn't invoked. However, it's usually even clearer (or at least more concise) in these situations to work with the continuation passing style directly.
-- fail directly using CPS fail :: String -> ContT r (Either String) a fail s = ContT $ \k -> Left s

On Sat, May 15, 2010 at 2:28 PM, Max Cantor
Where is my bind statement doing a case analysis? Isn't it just propagating, in a sense, the case analysis that came from values coming into the monad via return or via throwError?
What you did was reimplement the Either -type- not the Either -monad-. To see this lets make a complete interface to Either and provide the two implementations of that, now, abstract data type. Every function using Either can be written using the following interface: class EitherLike e where injLeft :: a -> e a b injRight :: b -> e a b either :: (a -> c) -> (b -> c) -> e a b -> c And here are two implementations: instance EitherLike Either where injLeft = Left injRight = Right either = Prelude.either type CEEither a b = forall c. (a -> c) -> (b -> c) -> c instance EitherLike CEEither where injLeft a = \l r -> l a injRight b = \l r -> r b either f g e = e f g Now we can write your functions and the "standard" Either monad definitions in terms of this abstract interface.
retErrCPS :: a -> ErrCPS e m a retErrCPS x = ErrCPS $ \_ good -> good x
return x = Right x retEither x = injRight x retErrCPS x = ErrCPS $ injRight x
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
bindErrCPS m f = ErrCPS $ either injLeft (runErrCPS . f) (runErrCPS m) Left e >>= _ = Left e Right a >>= f = f a bindEither m f = either injLeft f m So, modulo wrapping and unwrapping, the code is identical. In version of GHC prior to pointer tagging, a case analysis on Either would be implemented very much like the Church-encoded eliminator, i.e. in case e of Left a -> f a, Right b -> g b pre-pointer tagging GHC would push (essentially) f and g on a stack and enter e, e would then choose which of f or g to return to. So your representation is still doing a case analysis, it is just representing it a different way.
Also, why wouldn't callCC work here? I'm not that familiar with the ContT monad so any more details would be very much appreciated.
It's hard to implement a "global" abort with callCC. You can implement a local one easily by using an outer callCC to provide an "escape" continuation, but you have to explicitly pass around this "escape" continuation.

On Fri, May 14, 2010 at 4:25 PM, Derek Elkins
You did it wrong. All you did was Church encode the Either type. Your bind is still doing a case-analysis. All you have to do is use ContT r (Either e). The bind implementation for ContT is completely independent of the underlying monad. It doesn't even require the m in ContT r m to be a functor, let alone a monad. Therefore the ContT bind doesn't do any case-analysis because it doesn't know anything about the underlying monad. One way to look at what is happening is to compare it to Andrzej Filiniski's work in "Representing Monads" and "Representing Layered Monads".
Here's a bit more fleshed out version of what Derek is talking about, for those following along at home: http://hpaste.org/fastcgi/hpaste.fcgi/view?id=25515#a25515 Derek - should I be doing something smarter in 'catch'? I couldn't think of anything obvious. Antoine

On Sat, May 15, 2010 at 9:20 PM, Antoine Latter
On Fri, May 14, 2010 at 4:25 PM, Derek Elkins
wrote: You did it wrong. All you did was Church encode the Either type. Your bind is still doing a case-analysis. All you have to do is use ContT r (Either e). The bind implementation for ContT is completely independent of the underlying monad. It doesn't even require the m in ContT r m to be a functor, let alone a monad. Therefore the ContT bind doesn't do any case-analysis because it doesn't know anything about the underlying monad. One way to look at what is happening is to compare it to Andrzej Filiniski's work in "Representing Monads" and "Representing Layered Monads".
Here's a bit more fleshed out version of what Derek is talking about, for those following along at home:
http://hpaste.org/fastcgi/hpaste.fcgi/view?id=25515#a25515
Derek - should I be doing something smarter in 'catch'? I couldn't think of anything obvious.
No, that's pretty much what you should be doing also note, for conceptual purposes, that the const (Left e) is equivalent to (Left e
=). In "Representing Monads" to actually perform an effect it gets reified back into a data structure, in this case Either e a, manipulated as appropriate, then reflected back into an implicit effect. The reify function is just applying to the identity continuation so your catch can be written more clearly.
reify :: Monad m => ContT r m r -> m r reify m = runContT m return catch :: (e -> Error e a) -> Error e a -> Error e a catch handler m = case reify (unE m) of Left e -> handler e; Right a -> return a

On Fri, 14 May 2010, Derek Elkins wrote:
You did it wrong. All you did was Church encode the Either type. Your bind is still doing a case-analysis. All you have to do is use ContT r (Either e). The bind implementation for ContT is completely independent of the underlying monad. It doesn't even require the m in ContT r m to be a functor, let alone a monad. Therefore the ContT bind doesn't do any case-analysis because it doesn't know anything about the underlying monad. One way to look at what is happening is to compare it to Andrzej Filiniski's work in "Representing Monads" and "Representing Layered Monads".
What I don't get is that the bind operation for ContT and ErrCPS look so similar to me ContT (stripping off the newtype constructor/destructors): m >>= k = \c -> m (\a -> k a c) ErrCPS (stripping off the newtype constructor/destructors): m >>= f = \err good -> m err (\x -> f x err good) I don't get why one is slow but not the other? -- Russell O'Connor http://r6.ca/ ``All talk about `theft,''' the general counsel of the American Graphophone Company wrote, ``is the merest claptrap, for there exists no property in ideas musical, literary or artistic, except as defined by statute.''
participants (8)
-
Andrea Vezzosi
-
Antoine Latter
-
Derek Elkins
-
Henning Thielemann
-
Jan-Willem Maessen
-
Max Cantor
-
roconnor@theorem.ca
-
wren ng thornton