长度 vs 折叠 vs 显式递归的性能特征

标签 performance haskell recursion performance-testing fold

我写了六个版本的 length功能。一些性能差异是有道理的,但其中一些似乎根本不同意我读过的文章(例如 this onethis one )。

-- len1 and lenFold1 should have equivalent performance, right?

len1 :: [a] -> Integer
len1 [] = 0
len1 (x:xs) = len1 xs + 1

lenFold1 :: [a] -> Integer
lenFold1 = foldr (\_ a -> a + 1) 0


-- len2 and lenFold2 should have equivalent performance, right?

len2 :: [a] -> Integer
len2 xs = go xs 0 where
  go [] acc = acc
  go (x:xs) acc = go xs (1 + acc)

lenFold2 :: [a] -> Integer
lenFold2 = foldl (\a _ -> a + 1) 0


-- len3 and lenFold3 should have equivalent performance, right?
-- And len3 should outperform len1 and len2, right?

len3 :: [a] -> Integer
len3 xs = go xs 0 where
  go [] acc = acc
  go (x:xs) acc = go xs $! (1 + acc)

lenFold3 :: [a] -> Integer
lenFold3 = foldl' (\a _ -> a + 1) 0
我机器上的实际性能令人费解。
*Main Lib> :set +m +s
*Main Lib> xs = [1..10000000]
(0.01 secs, 351,256 bytes)
*Main Lib> len1 xs
10000000
(5.47 secs, 2,345,244,016 bytes)
*Main Lib> lenFold1 xs
10000000
(2.74 secs, 1,696,750,840 bytes)
*Main Lib> len2 xs
10000000
(6.02 secs, 2,980,997,432 bytes)
*Main Lib> lenFold2 xs
10000000
(3.97 secs, 1,776,750,816 bytes)
*Main Lib> len3 xs
10000000
(5.24 secs, 3,520,354,616 bytes)
*Main Lib> lenFold3 xs
10000000
(1.24 secs, 1,040,354,528 bytes)
*Main Lib> length xs
10000000
(0.21 secs, 720,354,480 bytes)
我的问题:
  • 为什么fold每个函数的版本始终优于使用显式递归的版本?
  • 尽管有 this article 的警告,但这些实现都没有在我的机器上达到堆栈溢出。 .为什么不?
  • 为什么不len3表现优于 len1len2 ?
  • 为什么 Prelude 的 length性能比这些实现中的任何一个都好?

  • 编辑:
    感谢 Carl 的建议,GHCI 默认解释代码这一事实解决了我的第一和第二个问题。再次运行 -fobject-code解释了显式递归和折叠之间的不同性能。新测量:
    Prelude Lib Main> xs = [1..10000000]
    (0.00 secs, 354,136 bytes)
    Prelude Lib Main> len1 xs
    10000000
    (1.62 secs, 1,612,661,544 bytes)
    Prelude Lib Main> lenFold1 xs
    10000000
    (1.62 secs, 1,692,661,552 bytes)
    Prelude Lib Main> len2 xs
    10000000
    (2.46 secs, 1,855,662,888 bytes)
    Prelude Lib Main> lenFold2 xs
    10000000
    (2.53 secs, 1,772,661,528 bytes)
    Prelude Lib Main> len3 xs
    10000000
    (0.48 secs, 1,680,361,272 bytes)
    Prelude Lib Main> lenFold3 xs
    10000000
    (0.31 secs, 1,040,361,240 bytes)
    Prelude Lib Main> length xs
    10000000
    (0.18 secs, 720,361,272 bytes)
    
    关于这个我还有几个问题。
  • 为什么lenFold3跑赢大盘len3 ?我跑了几次
  • 怎么样length仍然胜过所有这些实现吗?
  • 最佳答案

    我认为无论您尝试使用什么标志,都无法正确测试 GHCi 的性能。
    通常,对 Haskell 代码进行性能测试的最佳方法是使用 Criterion 基准测试库并使用 ghc -O2 进行编译。 .转换为 Criterion 基准,您的程序如下所示:

    import Criterion.Main
    import GHC.List
    import Prelude hiding (foldr, foldl, foldl', length)
    
    len1 :: [a] -> Integer
    len1 [] = 0
    len1 (x:xs) = len1 xs + 1
    
    lenFold1 :: [a] -> Integer
    lenFold1 = foldr (\_ a -> a + 1) 0
    
    len2 :: [a] -> Integer
    len2 xs = go xs 0 where
      go [] acc = acc
      go (x:xs) acc = go xs (1 + acc)
    
    lenFold2 :: [a] -> Integer
    lenFold2 = foldl (\a _ -> a + 1) 0
    
    len3 :: [a] -> Integer
    len3 xs = go xs 0 where
      go [] acc = acc
      go (x:xs) acc = go xs $! (1 + acc)
    
    lenFold3 :: [a] -> Integer
    lenFold3 = foldl' (\a _ -> a + 1) 0
    
    testLength :: ([Int] -> Integer) -> Integer
    testLength f = f [1..10000000]
    
    main = defaultMain
      [ bench "lenFold1" $ whnf testLength lenFold1
      , bench "len1" $ whnf testLength len1
      , bench "len2" $ whnf testLength len2
      , bench "lenFold2" $ whnf testLength lenFold2
      , bench "len3" $ whnf testLength len3
      , bench "lenFold3" $ whnf testLength lenFold3
      , bench "length" $ whnf testLength (fromIntegral . length)
      ]
    
    我机器上的缩写结果是:
    len1                 190.9 ms   (136.8 ms .. 238.6 ms)
    lenFold1             207.8 ms   (151.6 ms .. 248.6 ms)
    len2                 69.96 ms   (69.09 ms .. 71.63 ms)
    lenFold2             1.191 s    (917.1 ms .. 1.454 s)
    len3                 69.26 ms   (69.20 ms .. 69.35 ms)
    lenFold3             87.14 ms   (86.95 ms .. 87.35 ms)
    length               26.78 ms   (26.50 ms .. 27.08 ms)
    
    请注意,这些结果与您从 GHCi 运行这些测试所观察到的性能有很大不同,无论是绝对值还是相对值,无论是否使用 -fobject-code .为什么?甘拜下风。
    无论如何,基于这个适当的基准,len1lenFold1具有几乎相同的性能。实际上,为 lenFold1 生成的 Core是:
    lenFold1 = len1
    
    所以它们是相同的功能。不过,我的基准测试中的明显差异是真实的,它似乎是某些缓存/对齐问题的结果。如果我重新订购 len1lenFold1main ,性能差异翻转(因此 len1 是“慢的”)。len2len3也有相同的性能,因为它们是相同的功能。 (实际上, len3 的生成代码是 len3 = len2 。)GHC 的严格性分析器确定表达式 1 + acc即使没有显式 $! 也可以严格评估运算符(operator)。lenFold3稍慢,因为 foldl'不是内联的,所以组合函数每次都需要显式调用。这可以说是一个已报告的错误 here .我们可以通过改变 lenFold3 的定义来解决这个问题。向 foldl' 显式提供三个参数:
    lenFold3 xs = foldl' (\a _ -> a + 1) 0 xs
    
    然后它的表现和 len2 一样好和 len3 :
    lenFold3             66.99 ms   (66.76 ms .. 67.30 ms)
    
    lenFold2的糟糕表现是同样问题的体现。如果没有内联,GHC 就无法进行适当的优化。如果我们将定义改为:
    lenFold2 xs = foldl (\a _ -> a + 1) 0 xs
    
    它的表现和其他的一样好:
    lenFold2             66.64 ms   (66.58 ms .. 66.68 ms)
    
    需要明确的是,在对 lenFold2 进行这两项更改后和 lenFold3 ,功能len2 , len3 , lenFold2 , 和 lenFold3都是相同的,除了 lenFold2lenFold3申请 +运算符的顺序不同。如果我们使用定义:
    lenFold2 xs = foldl (\a _ -> 1 + a) 0 xs
    lenFold3 xs = foldl' (\a _ -> 1 + a) 0 xs
    
    那么生成的 Core(你可以用 ghc -O2 -ddump-simpl -dsuppress-all -dsuppress-uniques -fforce-recomp 查看)实际上是:
    len2 = ...actual definition...
    lenFold2 = len2
    len3 = len2
    lenFold3 = len2
    
    所以它们完全相同。
    它们与 len1 真的不同(或等效地 lenFold1 )因为 len1建立一大堆堆栈帧,当它到达列表的末尾并“发现”一个空列表的长度为零时,它需要处理这些帧。没有堆栈溢出的原因是很多关于 Haskell 堆栈溢出的博客文章似乎已经过时或基于 GHCi 测试。在使用现代 GHC 版本编译的代码中,maximum stack size默认为物理内存的 80%,因此您可以在不注意的情况下使用千兆字节的堆栈。在这种情况下,使用 +RTS -hT 进行一些快速分析显示单个 len1 [1..10000000] 的堆栈增长到大约 60-70 兆字节。 ,几乎不足以溢出任何东西。相比之下,len2家庭没有积累任何可观的堆栈。
    最后,原因length使它们全部消失的是它使用 Int 计算长度而不是 Integer .如果我将类型签名更改为:
    len1 :: [a] -> Int
    len2 :: [a] -> Int
    
    然后我得到:
    len1                 144.7 ms   (121.8 ms .. 157.9 ms)
    len2                 27.38 ms   (27.31 ms .. 27.44 ms)
    length               27.50 ms   (27.45 ms .. 27.54 ms)
    
    len2 (因此 lenFold2len3lenFold3 )都和 length 一样快.

    关于长度 vs 折叠 vs 显式递归的性能特征,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/63853024/

    相关文章:

    c++ - 跳表如何工作?

    performance - 何时在我的tal :condition?上使用nocall

    performance - Julia:使用恒定字段进行结构以优化性能

    python - 递归和二叉树

    java - 打印所有验证括号,这里的递归如何工作?

    scala - 为什么 scala.util.Success.apply 不是无限递归的?

    mysql - 为什么使用 cassandra 而不是 mysql 的 nosql?

    c - 将 Haskell 行翻译成 C

    lazy-evaluation - hGetContents 是如何实现内存效率的?

    haskell - 如何从多次运行的 haskell 基准测试中获取更有意义的统计数据