我做了一些Criterion基准测试,通过在monad堆栈上运行我的代码来估算我失去了多少性能。结果很奇怪,我可能在我的基准测试中偶然发现了一些懒惰的陷阱。
基准测试告诉我,即使不使用WriterT String IO
,运行IO
也比运行普通tell
慢20倍(!)。奇怪的是,如果我将WriterT
与ReaderT
和ContT
进行堆叠,那只会慢5倍。这可能是我的基准测试中的一个错误。我在这里做错了什么?
{-#LANGUAGE BangPatterns#-}
module Main where
import Criterion.Main
import Control.Monad
import Control.Monad.Writer
import Control.Monad.Reader
import Control.Monad.Cont
process :: Monad m => Int -> m Int
process = foldl (>=>) return (replicate 100000 (\(!x) -> return (x+1)))
test n = process n >> return ()
main = defaultMain [
bench "Plain" t0
,bench "Writer" t1
,bench "Reader" t2
,bench "Cont" t3
,bench "RWC" t4
]
t0 = test 1 :: IO ()
t1 = (runWriterT (test 1:: WriterT String IO ()) >> return ()) :: IO ()
t2 = (runReaderT (test 1:: ReaderT String IO ()) "" >> return ()) :: IO ()
t3 = (runContT (test 1:: ContT () IO ()) (return) >> return ()) :: IO ()
t4 = ((runWriterT . flip runReaderT "" . flip runContT return $
(test 1 :: ContT () (ReaderT String (WriterT String IO)) ())) >> return ()) :: IO ()
benchmarking Plain mean: 1.938814 ms, lb 1.846508 ms, ub 2.052165 ms, ci 0.950 std dev: 519.7248 us, lb 428.4684 us, ub 709.3670 us, ci 0.950 benchmarking Writer mean: 39.50431 ms, lb 38.25233 ms, ub 40.74437 ms, ci 0.950 std dev: 6.378220 ms, lb 5.738682 ms, ub 7.155760 ms, ci 0.950 benchmarking Reader mean: 12.52823 ms, lb 12.03947 ms, ub 13.09994 ms, ci 0.950 std dev: 2.706265 ms, lb 2.324519 ms, ub 3.462641 ms, ci 0.950 benchmarking Cont mean: 8.100272 ms, lb 7.634488 ms, ub 8.633348 ms, ci 0.950 std dev: 2.562829 ms, lb 2.281561 ms, ub 2.878463 ms, ci 0.950 benchmarking RWC mean: 9.871992 ms, lb 9.436721 ms, ub 10.37302 ms, ci 0.950 std dev: 2.387364 ms, lb 2.136819 ms, ub 2.721750 ms, ci 0.950
答案 0 :(得分:17)
正如你所注意到的那样,懒惰的作家单子格很慢。使用Daniel Fischer建议的严格版本可以提供很多帮助,但为什么在大堆栈中使用它会变得如此之快?
要回答这个问题,我们来看看这些变压器的实施情况。首先,懒惰的作家monad变换器。
newtype WriterT w m a = WriterT { runWriterT :: m (a, w) }
instance (Monoid w, Monad m) => Monad (WriterT w m) where
return a = WriterT $ return (a, mempty)
m >>= k = WriterT $ do
~(a, w) <- runWriterT m
~(b, w') <- runWriterT (k a)
return (b, w `mappend` w')
正如你所看到的,这确实很有用。它运行底层monad的动作,进行一些模式匹配并收集写入的值。几乎是你所期待的。严格版本是相似的,只是没有无可辩驳的(懒惰)模式。
newtype ReaderT r m a = ReaderT { runReaderT :: r -> m a }
instance (Monad m) => Monad (ReaderT r m) where
return = lift . return
m >>= k = ReaderT $ \ r -> do
a <- runReaderT m r
runReaderT (k a) r
读卡器变压器有点精简。它分配阅读器环境并调用底层monad来执行操作。这里没有惊喜。
现在,让我们看一下ContT
。
newtype ContT r m a = ContT { runContT :: (a -> m r) -> m r }
instance Monad (ContT r m) where
return a = ContT ($ a)
m >>= k = ContT $ \c -> runContT m (\a -> runContT (k a) c)
注意什么不同? 它实际上并没有使用底层monad中的任何函数!实际上,它甚至不需要m
成为monad。这意味着根本没有进行慢速模式匹配或追加。只有当您实际尝试从基础monad中解除任何操作时,ContT
才会使用其绑定运算符。
instance MonadTrans (ContT r) where
lift m = ContT (m >>=)
因此,由于您实际上并没有执行任何特定于编写器的内容,ContT
避免使用WriterT
中的慢速绑定运算符。这就是为什么在堆栈顶部放置ContT
会使速度快得多,以及为什么ContT () IO ()
的运行时间与深层堆栈的运行时间非常相似。
答案 1 :(得分:5)
Writer
极度放缓的一部分原因是你正在使用懒惰的作家monad,所以你的爆炸模式在那里并没有帮助,参见this question的答案有更详细的解释(虽然对于州,但这也是同样的原因)。将其更改为Control.Monad.Writer.Strict
可将此处的减速速度从八倍降低至小于四倍。堆栈更快,我还没理解为什么。