使用Haskell(++)运算符附加到列表会导致多个列表遍历吗?

时间:2015-03-31 16:47:50

标签: list haskell optimization time-complexity

附加到带有(++)的Haskell列表会导致列表被多次遍历吗?

我在GHCI尝试了一个简单的实验。

第一次运行:

$ ghci
GHCi, version 7.8.4: http://www.haskell.org/ghc/  :? for help
Prelude> let t = replicate 9999999 'a' ++ ['x'] in last t
'x'
(0.33 secs, 1129265584 bytes)

第二轮:

$ ghci
GHCi, version 7.8.4: http://www.haskell.org/ghc/  :? for help
Prelude> let t = replicate 9999999 'a' in last t
'a'
(0.18 secs, 568843816 bytes)

唯一的区别是++ ['x']将最后一个元素追加到列表中。它导致运行时从.18s增加到.33s,内存从568MB增加到1.12GB。

所以它似乎确实导致了多次遍历。有人可以在理论上证实吗?

2 个答案:

答案 0 :(得分:6)

您无法从这些数字中得出结论:第一次运行是进行两次遍历,还是一次遍历,其中每次执行需要更多时间并分配比第二次运行中的单次遍历更多的内存。

事实上,后者发生在这里。您可以考虑这样的两个评估:

  • 在第二个表达式let t = replicate 9999999 'a' in last t中,在每个步骤中但最后一个,last计算其参数,这会导致replicate分配一个cons单元并递减一个计数器,然后由last消耗cons单元格。

  • 在第一个表达式let t = replicate 9999999 'a' ++ ['x'] in last t中,在每个步骤中,但最后一个,last计算其参数,这会导致(++)计算其第一个参数,从而导致{{ 1}}分配一个cons单元并减少一个计数器,然后由replicate消耗该cons单元,(++)分配一个新的cons单元,然后{{1}消耗该新的cons单元}}

所以第一个表达式仍然只是一个遍历,它只是一个每步完成更多工作的表达式。

现在,如果你愿意,你可以把所有这些工作分成{&#34} (++)"和#34;由last"完成的工作并称这两个"遍历&#34 ;;这对于理解程序完成的工作总量来说是一种有用的方法。但是由于哈斯克尔的懒惰,两次"遍历"如上所述,它们实际上是交错的,因此大多数人会说列表只被遍历一次。

答案 1 :(得分:4)

我想谈谈在启用优化时会发生什么,因为它可以非常彻底地转换程序的性能特征。我将查看ghc -O2 Main.hs -ddump-simpl -dsuppress-all生成的Core输出。此外,我使用+RTS -s运行已编译的程序以获取有关内存使用情况和运行时间的信息。

使用 GHC 7.8.4 ,代码的两个版本在相同的时间内运行并具有相同的堆分配量。这是因为replicate 9999999 'a'++ ['x']被替换为genlist 9999999,其中genlist如下所示(不完全相同,因为我使用原始Core的自由翻译):

genlist :: Int -> [Char]
genlist n | n <= 1 = "ax"
          | otherwise = 'a' : genList (n - 1)

由于我们只在一个步骤中进行生成和连接,因此我们只分配一次列表单元格。

使用 GHC 7.10.1 ,我们为列表处理获得了新的优化。现在我们的两个程序都分配了与print $ "Hello World"程序一样多的内存(在我的机器上大约52 Kb)。这是因为我们完全跳过列表创建。现在last也被融合了;我们转而致电getlast 9999999getlast为:

getlast :: Int -> Char
getlast 1 = 'x'
getlast n = getlast (n - 1)

在可执行文件中,我们将有一个小型机器代码循环,从99999991倒计时。 GHC不够智能,不能跳过所有计算并直接返回'x',但它确实做得很好,最后它给了我们一些与原始代码完全不同的东西。