[英]Performance characteristics of length vs fold vs explicit recursion
我已经编写了length
函数的六个版本。 一些性能差异是有道理的,但其中一些似乎根本不同意我读过的文章(例如this one和this 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
版本始终优于使用显式递归的版本?len3
性能不如len1
或len2
?length
比这些实现中的任何一个都表现得更好?编辑:
感谢 Carl 的建议,GHCI 默认解释代码这一事实解决了我的第一和第二个问题。 使用-fobject-code
再次运行它可以-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
。 为什么? 甘拜下风。
无论如何,基于这个适当的基准, len1
和lenFold1
具有几乎相同的性能。 实际上,为lenFold1
生成的 Core 是:
lenFold1 = len1
所以它们是相同的功能。 不过,我的基准测试中的明显差异是真实的,而且似乎是某些缓存/对齐问题的结果。 如果我在main
中对len1
和lenFold1
重新排序,性能差异就会翻转(因此len1
是“慢的”)。
len2
和len3
也具有相同的性能,因为它们是相同的功能。 (实际上,为len3
生成的代码是len3 = len2
。)GHC 的严格性分析器确定表达式1 + acc
可以严格计算,即使没有显式的$!
操作员。
lenFold3
稍微慢一些,因为foldl'
没有内联,所以每次通过组合函数都需要显式调用。 这可以说是这里报告的一个错误。 我们可以通过更改lenFold3
的定义来明确地为foldl'
提供三个参数来解决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
都是相同的,只是lenFold2
和lenFold3
以不同的顺序应用+
运算符。 如果我们使用定义:
lenFold2 xs = foldl (\a _ -> 1 + a) 0 xs
lenFold3 xs = foldl' (\a _ -> 1 + a) 0 xs
那么生成的核心(您可以使用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 版本编译的代码中, 最大堆栈大小默认为物理内存的 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
(等lenFold2
, len3
,和lenFold3
)都尽可能快地length
。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.