繁体   English   中英

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

[英]Performance characteristics of length vs fold vs explicit recursion

我已经编写了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)

我的问题:

  1. 为什么每个函数的fold版本始终优于使用显式递归的版本?
  2. 尽管有本文的警告,但这些实现都没有在我的机器上达到堆栈溢出。 为什么不?
  3. 为什么len3性能不如len1len2
  4. 为什么 Prelude 的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)

关于这个我还有几个问题。

  1. 为什么lenFold3优于len3 我跑了几次
  2. 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

所以它们是相同的功能。 不过,我的基准测试中的明显差异是真实的,而且似乎是某些缓存/对齐问题的结果。 如果我在main中对len1lenFold1重新排序,性能差异就会翻转(因此len1是“慢的”)。

len2len3也具有相同的性能,因为它们是相同的功能。 (实际上,为len3生成的代码是len3 = len2 。)GHC 的严格性分析器确定表达式1 + acc可以严格计算,即使没有显式的$! 操作员。

lenFold3稍微慢一些,因为foldl'没有内联,所以每次通过组合函数都需要显式调用。 这可以说是这里报告的一个错误。 我们可以通过更改lenFold3的定义来明确地为foldl'提供三个参数来解决foldl'

lenFold3 xs = foldl' (\a _ -> a + 1) 0 xs

然后它的表现与len2len3一样好:

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)

需要明确的是,在对lenFold2lenFold3进行这两个更改后,函数len2len3lenFold2lenFold3都是相同的,只是lenFold2lenFold3以不同的顺序应用+运算符。 如果我们使用定义:

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 (等lenFold2len3 ,和lenFold3 )都尽可能快地length

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM