13

我做了一些 Criterion 基准测试来估计通过在 monad 堆栈上运行我的代码会损失多少性能。结果相当奇怪,我可能在我的基准测试中偶然发现了一些懒惰的陷阱。

基准测试告诉我,运行WriterT String IO比普通运行慢 20 倍(!)IO,即使不使用tell. 奇怪的是,如果我堆叠WriterT起来ReaderTContT它只会慢 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 ()

结果

标杆平原
平均值:1.938814 ms,lb 1.846508 ms,ub 2.052165 ms,ci 0.950
标准开发:519.7248 us,lb 428.4684 us,ub 709.3670 us,ci 0.950

基准测试作家
平均值:39.50431 ms,lb 38.25233 ms,ub 40.74437 ms,ci 0.950
标准开发:6.378220 毫秒,磅 5.738682 毫秒,ub 7.155760 毫秒,ci 0.950

基准阅读器
平均值:12.52823 ms,lb 12.03947 ms,ub 13.09994 ms,ci 0.950
标准开发:2.706265 毫秒,磅 2.324519 毫秒,ub 3.462641 毫秒,ci 0.950

基准测试续
平均值:8.100272 ms,lb 7.634488 ms,ub 8.633348 ms,ci 0.950
标准开发:2.562829 毫秒,磅 2.281561 毫秒,ub 2.878463 毫秒,ci 0.950

基准 RWC
平均值:9.871992 ms,lb 9.436721 ms,ub 10.37302 ms,ci 0.950
标准开发:2.387364 ms,lb 2.136819 ms,ub 2.721750 ms,ci 0.950
4

2 回答 2

17

正如您所注意到的,懒惰的 writer monad 非常慢。使用 Daniel Fischer 建议的严格版本有很大帮助,但为什么在大堆栈中使用它会变得如此之快?

为了回答这个问题,我们来看看这些转换器的实现。首先,懒惰的作家单子变压器。

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 中解除任何操作时,才会ContT使用它的绑定运算符。

instance MonadTrans (ContT r) where
    lift m = ContT (m >>=)

因此,由于您实际上并没有做任何特定于编写ContT器的事情,因此请避免使用来自WriterT. 这就是为什么ContT在你的堆栈之上让它变得如此之快,以及为什么它的运行时间ContT () IO ()与更深的堆栈的运行时间如此相似。

于 2011-11-11T12:59:40.707 回答
5

极端减速的部分Writer原因是您正在使用惰性编写器 monad,因此您的 bang-pattern 在那里根本没有帮助,参见。这个问题的答案以获得更详细的解释(虽然对于State,但这里的原因相同)。改变它以Control.Monad.Writer.Strict将这里的减速从八倍减少到不到四倍。堆栈仍然更快,我还不明白为什么。

于 2011-11-11T11:33:36.797 回答