GHC Haskell中的记忆何时自动执行?

GHC Haskell中的记忆何时自动执行?,haskell,ghc,memoization,Haskell,Ghc,Memoization,我不明白为什么m1显然是记忆的,而m2不在下面: m1 = ((filter odd [1..]) !!) m2 n = ((filter odd [1..]) !! n) m1 10000000在第一次调用时大约需要1.5秒,而在后续调用时只需要1.5秒的一小部分(可能它会缓存列表),而m2 10000000总是需要相同的时间(用每次调用重建列表)。知道发生了什么吗?关于GHC是否以及何时将记忆一个函数,是否有经验法则?谢谢。GHC不会记忆功能 但是,每次输入其周围的lam

我不明白为什么m1显然是记忆的,而m2不在下面:

m1      = ((filter odd [1..]) !!)

m2 n    = ((filter odd [1..]) !! n)

m1 10000000在第一次调用时大约需要1.5秒,而在后续调用时只需要1.5秒的一小部分(可能它会缓存列表),而m2 10000000总是需要相同的时间(用每次调用重建列表)。知道发生了什么吗?关于GHC是否以及何时将记忆一个函数,是否有经验法则?谢谢。

GHC不会记忆功能

但是,每次输入其周围的lambda表达式时,它最多计算一次代码中的任何给定表达式,或者如果它处于顶层,则最多计算一次。在示例中使用语法糖时,确定lambda表达式的位置可能有点棘手,因此让我们将其转换为等效的去糖语法:

m1' = (!!) (filter odd [1..])              -- NB: See below!
m2' = \n -> (!!) (filter odd [1..]) n
(注:Haskell 98报告实际上描述了一个左操作符部分,如
(a%)
,相当于
\b->()a b
,但GHC将其分解为
()a
。这些在技术上是不同的,因为它们可以通过
seq
来区分。我想我可能已经提交了一份GHC Trac票证。)

鉴于此,您可以看到在
m1'
中,表达式
filter odd[1..]
不包含在任何lambda表达式中,因此它在程序每次运行时只计算一次,而在
m2'
中,
filter odd[1..]
将在每次输入lambda表达式时计算,即。,每次调用
m2'
。这就解释了你所看到的时间上的差异


事实上,某些版本的GHC具有某些优化选项,共享的值将超过上述描述所示。在某些情况下,这可能会有问题。例如,考虑函数

f = \x -> let y = [1..30000000] in foldl' (+) 0 (y ++ [x])
GHC可能会注意到
y
不依赖于
x
,并将函数重写为

f = let y = [1..30000000] in \x -> foldl' (+) 0 (y ++ [x])

在这种情况下,新版本的效率要低得多,因为它必须从存储
y
的内存中读取大约1GB的数据,而原始版本将在恒定的空间中运行,并适合处理器的缓存。事实上,在GHC 6.12.1下,函数
f
在未经优化的情况下编译时的速度几乎是使用
-O2

编译时的两倍。m1只计算一次,因为它是一个常量应用形式,而m2不是CAF,因此每次计算都要计算一次


参见CAFs上的GHC维基:

这两种形式之间有一个至关重要的区别:单态限制适用于m1,但不适用于m2,因为m2明确给出了参数。所以m2的类型是一般的,而m1的类型是特殊的。分配给它们的类型有:

m1 :: Int -> Integer
m2 :: (Integral a) => Int -> a

大多数Haskell编译器和解释器(实际上我所知道的所有这些)都不会记忆多态结构,因此每次调用m2时都会重新创建m2的内部列表,而m1则不是。我不确定,因为我对Haskell自己来说是个新手,但似乎是因为第二个函数是参数化的,而第一个函数不是。函数的本质是,它的结果取决于输入值,特别是在函数范式中,它只取决于输入。显然,没有参数的函数总是一次又一次地返回相同的值,不管发生什么

另外,GHC编译器中有一种优化机制,它利用这一事实在整个程序运行时只计算一次这样一个函数的值。当然,它做得很懒,但还是做得很好。当我编写以下函数时,我自己也注意到了这一点:

primes = filter isPrime [2..]
    where isPrime n = null [factor | factor <- [2..n-1], factor `divides` n]
        where f `divides` n = (n `mod` f) == 0
primes=过滤器isPrime[2..]

其中isPrime n=null[factor | factor解释“m1只计算一次,因为它是一个常量应用形式”对我来说没有意义。因为m1和m2可能都是顶级变量,我认为这些函数只计算一次,不管它们是否是CAF。区别在于列表是否
[1..]
在程序执行期间只计算一次,或者在函数的每个应用程序中计算一次,但它是否与CAF相关?从链接页面:“一个CAF…可以被编译成一个供所有用户共享的图形,也可以被编译成一些共享代码,这些代码在第一次计算时会被某个图形覆盖”。由于
m1
是一个CAF,第二个应用并
过滤奇数[1..]
(不仅仅是
[1..]
)仅计算一次。GHC还可以注意到,
m2
指的是
filter odd[1..]
,并将链接放置到
m1
中使用的相同thunk,但这不是一个好主意:在某些情况下,它可能导致大量内存泄漏。@Alexey:感谢您对
[1..]
filter odd[1..]的更正
。对于其余部分,我仍然不相信。如果我没有弄错的话,CAF只在我们想争论编译器可以用全局thunk(可能与
m1
中使用的thunk相同)替换
m2
中的
过滤器奇数[1..]
时才有意义。但在asker的情况下,编译器没有这样做“优化”,我看不出它与问题的相关性。它可以在
m1
中替换它,这是相关的。计算(filter odd[1..])表达式的成本无论如何都接近于零——毕竟它是一个惰性列表,所以实际成本在(x!!10000000)中在实际评估列表时应用。此外,m1和m2似乎仅在以下测试中使用-O2和-O1(在我的ghc 6.12.3上)评估一次:(测试=m1 10000000
seq
m1 10000000)。虽然没有指定优化标志,但存在差异。并且“f”的两个变体“顺便说一句,不管优化如何,最大驻留时间为5356字节(使用-O2时总分配更少)。@Ed'ka:使用上面的
f定义,试试这个测试程序。”