我正在尝试不同的方法来获得Fibonacci序列的给定索引处的数字,它们基本上可以分为两类:
我选择了两个例子:
fibs1 :: Int -> Integer
fibs1 n = fibs1' !! n
where fibs1' = 0 : scanl (+) 1 fibs1'
fib2 :: Int -> Integer
fib2 n = fib2' 1 1 n where
fib2' _ b 2 = b
fib2' a b n = fib2' b (a + b) (n - 1)
fibs1:
real 0m2.356s
user 0m2.310s
sys 0m0.030s
fibs2:
real 0m0.671s
user 0m0.667s
sys 0m0.000s
两者都是用64位GHC 7.6.1和-O2 -fllvm
编译的。他们的核心转储在长度上非常相似,但它们在我不太熟练解释的部分有所不同。
对于n = 350000(fibs1
)Stack space overflow
失败,我并不感到惊讶。但是,我对它使用了那么多内存的事实感到不舒服。
我想澄清一些事情:
答案 0 :(得分:4)
为什么GC在整个计算过程中不会处理列表的开头,即使它大部分都很快变得无用?
fibs1
使用大量内存并且速度很慢,因为scanl
是惰性的,它不会评估列表元素,所以
fibs1' = 0 : scanl (+) 1 fibs1'
产生
0 : scanl (+) 1 (0 : more)
0 : 1 : let f2 = 1+0 in scanl (+) f2 (1 : more')
0 : 1 : let f2 = 1+0 in f2 : let f3 = f2+1 in scanl (+) f3 (f2 : more'')
0 : 1 : let f2 = 1+0 in f2 : let f3 = f2+1 in f3 : let f4 = f3+f2 in scanl (+) f4 (f3 : more''')
等。所以你很快就会得到一个巨大的嵌套thunk。当评估该thunk时,它被推入堆栈,并且在250000和350000之间的某个点上,它对于默认堆栈而言变得太大。
由于每个list元素在未评估时都保存对前一个的引用,因此列表的开头不能被垃圾回收。
如果您使用严格扫描,
fibs1 :: Int -> Integer
fibs1 n = fibs1' !! n
where
fibs1' = 0 : scanl' (+) 1 fibs1'
scanl' f a (x:xs) = let x' = f a x in x' `seq` (a : scanl' f x' xs)
scanl' _ a [] = [a]
当生成k
- 列表单元格时,其值已经被评估,因此不会引用前一个,因此列表可以被垃圾收集(假设没有其他任何内容可以引用它)它被遍历了。
通过该实现,列表版本与fib2
一样快且精简(它需要分配列表单元格,因此它分配更多一点,因此可能会慢一点,但是差异很小,因为Fibonacci数字变得如此之大,以至于列表构造开销变得微不足道了。)
scanl
的想法是,它的结果会逐渐消耗,因此消耗会强制元素并阻止大量thunk的累积。
为什么GHC没有将列表版本优化为变量版本,因为一次只需要两个元素?
它的优化器无法通过算法来确定。 scanl
对编译器不透明,它不知道scanl
做了什么。
如果我们采用scanl
的确切源代码(在Prelude中重命名或隐藏scanl
,我选择重命名),
scans :: (b -> a -> b) -> b -> [a] -> [b]
scans f q ls = q : (case ls of
[] -> []
x:xs -> scans f (f q x) xs)
并编译导出它的模块(使用-O2),然后使用
查看生成的接口文件ghc --show-iface Scan.hi
我们得到(例如,编译器版本之间的细微差别)
Magic: Wanted 33214052,
got 33214052
Version: Wanted [7, 0, 6, 1],
got [7, 0, 6, 1]
Way: Wanted [],
got []
interface main:Scan 7061
interface hash: ef57dac14815e2f1f897b42a007c0c81
ABI hash: 8cfc8dab79de6a51fcad666f1869574f
export-list hash: 57d6805e5f0b5f76f0dd8dfb228df988
orphan hash: 693e9af84d3dfcc71e640e005bdc5e2e
flag hash: 1e8135cb44ef6dd330f1f56943d1f463
used TH splices: False
where
exports:
Scan.scans
module dependencies:
package dependencies: base* ghc-prim integer-gmp
orphans: base:GHC.Base base:GHC.Float base:GHC.Real
family instance modules:
import -/ base:Prelude 1cb4b618cf45281dc97748b1831bf0cd
d79ca4e223c0de0a770a3b88a5e67687
scans :: forall b a. (b -> a -> b) -> b -> [a] -> [b]
{- Arity: 3, HasNoCafRefs, Strictness: LLL -}
vectorised variables:
vectorised tycons:
vectorised reused tycons:
scalar variables:
scalar tycons:
trusted: safe-inferred
require own pkg trusted: False
并且看到接口文件没有公开函数的展开,只有它的类型,arity,严格性以及它没有引用CAF。
当编译导入的模块时,编译器必须经历的全部是接口文件公开的信息。
在这里,没有暴露的信息允许编译器执行任何其他操作,但会调用该函数。
如果展开了展开,编译器有机会内联展开并分析知道类型和组合函数的代码,以生成更多不会构建thunk的急切代码。
然而,scanl
的语义最大是惰性的,输出的每个元素都在检查输入列表之前发出。这导致GHC不能使加法严格,因为如果列表包含任何未定义的值,那将改变结果:
scanl (+) 1 [undefined] = 1 : scanl (+) (1 + undefined) [] = 1 : (1 + undefined) : []
,而
scanl' (+) 1 [undefined] = let x' = 1 + undefined in x' `seq` 1 : scanl' (+) x' []
= *** Exception: Prelude.undefined
可以制作变体
scanl'' f b (x:xs) = b `seq` b : scanl'' f (f b x) xs
对于上面的输入会产生1 : *** Exception: Prelude.undefined
,但是如果列表包含未定义的值,任何严格都会改变结果,所以即使编译器知道展开,它也不能使评估严格 - 除非它可以证明列表中没有未定义的值,这一事实对我们来说是显而易见的,但不是编译器[我不认为教编译器认识到并且能够证明没有未定义的值]。