【问题标题】:Why is `filterM + mapM_` so much slower than `mapM_ + when`, with large lists?为什么 `filterM + mapM_` 比 `mapM_ + when` 慢得多,列表很大?
【发布时间】:2021-06-21 03:28:05
【问题描述】:

我不太了解 Haskell 优化在内部是如何工作的,但我一直在使用过滤器,非常希望它们被优化成类似于 C++ 中的简单 if 的东西。例如

mapM_ print $ filter (\n -> n `mod` 2 == 0) [0..10]

将编译成等价于

for (int i = 0; i < 10; i++)
    if (i%2 == 0)
        printf("%d\n", i);

对于长列表(10 000 000 个元素),基本的filter 似乎是正确的,但如果我使用单子filterM,会有很大的不同。我为此速度测试编写了一段代码,很明显filterM 的使用持续时间比使用when 的命令式方法要长得多(250 倍)。

import Data.Array.IO
import Control.Monad
import System.CPUTime

main :: IO ()
main = do
  start <- getCPUTime
  arr <- newArray (0, 100) 0 :: IO (IOUArray Int Int)
  let
    okSimple i =
      i < 100

    ok i = do
      return $ i < 100
    -- -- of course we don't need IO for a simple i < 100
    -- -- but my goal is to ask for the contents of the array, e.g.
    -- ok i = do
    --   current <- readArray arr (i `mod` 101)
    --   return$ i `mod` 37 > current `mod` 37
    
    write :: Int -> IO ()
    write i =
      writeArray arr (i `mod` 101) i

    writeIfOkSimple :: Int -> IO ()
    writeIfOkSimple i =
      when (okSimple i) $ write i

    writeIfOk :: Int -> IO ()
    writeIfOk i =
      ok i >>= (\isOk -> when isOk $ write i)

  -------------------------------------------------------------------
  ---- these four methods have approximately same execution time ----
  ---- (but the last one is executed on 250 times shorter list)  ----
  -------------------------------------------------------------------
  -- mapM_ write$ filter okSimple [0..10000000*250] -- t = 20.694
  -- mapM_ writeIfOkSimple [0..10000000*250]        -- t = 20.698
  -- mapM_ writeIfOk [0..10000000*250]              -- t = 20.669
  filterM ok [0..10000000] >>= mapM_ write          -- t = 17.200

  -- evaluate array
  elems <- getElems arr
  print $ sum elems

  end <- getCPUTime
  print $ fromIntegral (end - start) / (10^12)

我的问题是:这两种方法(使用writeIfOk / 使用filterM okwrite)不应该编译成相同的代码(迭代列表、询问条件、写入数据)吗?如果没有,我是否可以做一些事情(重写代码、添加编译标志、使用内联编译指示或其他东西)以使它们在计算上等效,或者在性能至关重要时我是否应该始终使用 when

【问题讨论】:

  • 你的最后一次测试只适用于10000000,而不是10000000*250
  • filterM 需要更多时间这一事实并不奇怪:它不受 list fusion 的约束,而且会导致大量的包装和展开在IO 对象中,如果没有得到有效优化。
  • 您实际上是在要求编译器发现 ok 没有副作用,尽管其签名中有 IO,并针对这种情况进行优化以启用流式传输。编译器可能会使用足够高级的静态分析来做到这一点,但当前的 GHC 不会。我想这种情况并不常见,因为 Haskell 程序员往往不会将实际上纯粹的东西标记为IO,并且尽可能使用非IO 例程。
  • @WillemVanOnsem,列表融合在这里无关紧要。
  • @chi,这还需要一些非常花哨的终止分析才能看到filterM 将始终成功完成。

标签: performance haskell optimization monads io-monad


【解决方案1】:

把这个问题归结为本质,你问的区别

f (filter g xs)

f =<< filterM (pure . g) xs

这基本上归结为懒惰。 filter g xs 会根据需要逐步生成其结果,只需将 xs 步行足够远即可找到结果的下一个元素。 filterM 的定义如下:

filterM _p [] = pure []
filterM p (x : xs)
  = liftA2 (\q r -> if q then x : r else r)
           (p x)
           (filterM p xs)

由于IO 是一个“严格”应用程序,因此在遍历整个列表之前不会产生任何东西,将p x 结果累积到内存中。

【讨论】:

  • 我不确定我是否认同这个解释。如果它的成本增加 50%,甚至可能增加 300%,我也许可以……但是 25000% 只是为了将整个列表放入内存?
  • @DanielWagner,完全! GC 成本将飙升。
  • @DanielWagner,缓存未命中率也会上升。
  • @WillNess 它是二次方的,因为单个 GC 所花费的时间与实时数据量成正比,而您需要执行的 GC 次数与正在构建的数据结构的大小成正比。如果您在构建时丢弃结构,“实时数据量”为 O(1);但如果必须全部保留,“实时数据量”为 O(n)。
  • 搜索“GHC托儿所”会出现各种各样的东西,感谢您的参考。 :)
猜你喜欢
  • 2023-03-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-12-26
  • 2011-01-25
  • 2012-12-20
  • 1970-01-01
相关资源
最近更新 更多