Wow, I’m pretty surprised by those results.
My diagnosis is that the countdown benchmark is not that great to show the difference between sp/ev and eff. The reason is that the bind doesn’t really get in the way of other optimizations. Here’s another benchmark adapted from @lexi.lambda’s talk at 43:36:
lookupResult :: Int -> Maybe Int
{-# NOINLINE lookupResult #-}
lookupResult n
| n >= 0 = Just n
| otherwise = Nothing
programSp :: S.Error String S.:> es => Int -> S.Eff es Int
programSp n = do
nums <-
case lookupResult n of
Nothing -> S.throw "not found"
Just val -> return [1..val]
return $ sum nums
{-# NOINLINE programSp #-}
fooSp :: Int -> Either String Int
fooSp n = S.runEff $ S.runError (programSp n)
This has been designed such that the monadic bind actually breaks the fusion of the list producer [1..val]
and the consumer sum
. My hypothesis (and Alexis’ implicit claim) is that eff
still allows GHC to fuse these two operations while mtl (and maybe all other effect systems?) don’t.
Unfortunately, I don’t have easy access to a version of GHC with the delimited continuations primops, so I can’t really confirm that eff
is much faster. But I have been able to confirm that sp does in fact obstruct the list fusion.
Although it does seem like this problem doesn’t necessarily require the delimited continuation primops. Looking at the core that GHC produces, it seems like GHC just missing an optimization akin to the state hack for IO.