【发布时间】:2017-02-18 22:12:30
【问题描述】:
免责声明:前面可能有一些可疑的 Haskell。
我已经实现了 2 个版本的 Fibonacci,一个是线性和尾递归的(包括一个 banged 变体),另一个是使用经典的惰性列表方法:
{-# LANGUAGE BangPatterns #-}
-- Linear
fibLinear :: Int -> Integer
fibLinear 0 = 0
fibLinear 1 = 1
fibLinear x = fibLin' 2 1 2
where
fibLin' i y z | i == x = y
fibLin' i y z = fibLin' (i+1) z (z+y)
-- Linear banged
fibLinearBang :: Int -> Integer
fibLinearBang 0 = 0
fibLinearBang 1 = 1
fibLinearBang x = fibLin' 2 1 2
where
fibLin' (!i) (!y) (!z) | i == x = y
fibLin' (!i) (!y) (!z) = fibLin' (i+1) z (z+y)
-- Haskeller classic lazy
fibClassic :: Int -> Integer
fibClassic n = fib' !! n
where fib' = 0:1:zipWith (+) fib' (tail fib')
当使用 criterion 对其进行基准测试 (code) 时,我得到了一些不直观的结果:fibLinear 的 Fib 30 需要 443.0 ns,fibLinearBang 需要 279.5 ns,而 fibClassic 需要 73.51 ns。这很奇怪,因为我认为 fibLinear 和 fibLinearBang 可以优化成循环或跳转之类的东西。
我唯一的猜测是 fibClassic 中的 fib' 可能被记忆为一个常量列表,尽管它位于函数定义中,以便函数的后续调用重用相同的列表进行查找。
有人可以对我的基准测试结果给出明确的解释吗?
对于那些感兴趣的人,我已将项目放在Github。
【问题讨论】:
-
我会尝试使
fibLin'的所有论点严格并再次进行基准测试。你可以这样做,例如在第一种情况下使用fibLin' (!_) (!_) (!_) | ...,并启用相对扩展BangPatterns。 -
您的优化级别是多少?尾递归仅在严格时才像循环性能一样起作用,而 GHC 可能只会使您的代码在某些优化级别上严格。懒惰的版本绝对不会被记忆,我不确定这有多大帮助(你不需要重新计算列表的开头,但你仍然需要迭代它和计算不需要那么多时间)。默认情况下它相对较快。
-
还有为什么
fibLin'会产生一个元组?我没有看到你使用过它的第二个元素。 -
严格化
i是没有意义的,因为在每一步都与x进行比较,因此是强制的。但是其他参数确实可能是大笨蛋,但是如果 Core 包含Int#s 而不是Ints 并且因此刘海无济于事,我不会感到惊讶。但是您还在每个递归步骤中将x与0和1进行比较,这是多余的:您可以只做一个。而且完全懒惰真的不能记住fib'吗?不过,我认为这样的记忆不会提高性能,甚至根本不会。 -
您的基准测试并不是您认为的基准测试 - 尝试
fib 100k或其他一些较大的值。对于单次调用,“经典惰性”版本分配更多内存,耗时 5 倍(在我的机器上,n=80k)并且有近 80% 的 GC 时间(相比之下,fibBang为 5%。标准报告说@ 987654346@ 运行得更快,因为fib'浮动到顶层并保留给fibClassic的所有调用(看看核心!)。如果这个结果仍然令人惊讶,请回想一下 GHC Haskell 已针对惰性数据进行了高度优化结构。
标签: haskell recursion optimization tail-recursion microbenchmark