algorithm - 为什么对本地列表求和比用 `GHC -O2` 对教堂编码列表求和慢?

标签 algorithm performance list haskell

为了测试 church-encoded lists 对 user-defiend lists 和 native lists 的表现如何,我准备了 3 个基准:

用户定义的列表

data List a = Cons a (List a) | Nil deriving Show
lenumTil n        = go n Nil where
    go 0 result   = result
    go n result   = go (n-1) (Cons (n-1) result)
lsum Nil          = 0
lsum (Cons h t)   = h + (lsum t)

main = print (lsum (lenumTil (100000000 :: Int)))

原生列表

main = print $ sum ([0..100000000-1] :: [Int])

教会名单

fsum   = (\ a -> (a (+) 0))
fenumTil n cons nil = go n nil where
    go 0 result    = result
    go n result    = go (n-1) (cons (n-1) result)
main = print $ (fsum (fenumTil (100000000 :: Int)) :: Int)

基准测试结果出乎意料:

用户定义的列表

-- 4999999950000000
-- real 0m22.520s
-- user 0m59.815s
-- sys  0m20.327s

原生列表

-- 4999999950000000
-- real 0m0.999s
-- user 0m1.357s
-- sys  0m0.252s

教会名单

-- 4999999950000000
-- real 0m0.010s
-- user 0m0.002s
-- sys  0m0.003s

人们会期望,随着针对原生列表的大量特定优化,它们会表现最好。然而,教会名单的表现优于它们 100 倍,并且优于用户定义的 ADT 2250 倍。我用 GHC -O2 编译了所有程序。我尝试用 foldl' 替换 sum,结果相同。我尝试添加用户输入以确保教堂列表版本未优化为常量。 arkeet 在#haskell 上指出,通过检查 Core, native 版本有一个中间列表,但为什么呢?使用额外的 reverse 强制分配,所有 3 个执行大致相同。

最佳答案

GHC 7.10 有 call arity分析,它让我们根据 foldr 定义 foldl,从而让左折叠(包括 sum)参与融合。 GHC 7.8 还用 foldl 定义了 sum 但它不能将列表融合在一起。因此 GHC 7.10 的性能最佳且与 Church 版本相同。

Church 版本在任一 GHC 版本中进行优化都是小菜一碟。我们只需要将 (+)0 内联到 fenumTil 中,然后我们就有了一个明显的尾递归 go 它可以很容易地拆箱,然后由代码生成器变成一个循环。

用户定义的版本不是尾递归的,它在线性空间中工作,这当然会破坏性能。

关于algorithm - 为什么对本地列表求和比用 `GHC -O2` 对教堂编码列表求和慢?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32087530/

相关文章:

c# - 递归边搜索

java - 高 GC、CPU Activity 和元空间似乎没有增加,Web 应用程序执行变得无响应

c - 为什么 _mm512_store_pd 在这个矩阵乘法代码中 super 慢?

python - 列出 __iadd__、+= 和返回值

algorithm - 递归关系,算法

algorithm - 如何制作GPS交通应用程序?

algorithm - 为什么这个动态规划递归关系是正确的?

css - 子选择器和后代选择器在性能上有区别吗?

java - 如何使用 ListIterator 编辑列表内容

list - 是否 `(' a' :_)` represent a tuple or a list?