【问题标题】:Odd results from monad transformer benchmark. A bug?来自 monad 变压器基准测试的奇怪结果。一个错误?
【发布时间】:2011-12-26 22:13:42
【问题描述】:

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

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

【问题讨论】:

  • 你是用-O还是-O2编译的?

标签: performance haskell benchmarking monad-transformers


【解决方案1】:

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

【讨论】:

    【解决方案2】:

    正如您所注意到的,懒惰的 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。这意味着根本没有进行缓慢的模式匹配或追加。只有当您真正尝试从底层 monad 解除任何操作时,ContT 才会使用它的绑定运算符。

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

    因此,由于您实际上并未执行任何特定于编写器的操作,ContT 避免使用来自WriterT 的慢速绑定运算符。这就是为什么在堆栈顶部放置ContT 会使其速度更快,以及为什么ContT () IO () 的运行时间与更深堆栈的运行时间如此相似。

    【讨论】:

      猜你喜欢
      • 2012-06-27
      • 2013-11-22
      • 1970-01-01
      • 2015-10-21
      • 2023-04-02
      • 1970-01-01
      • 2014-04-13
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多