GHC Haskell中的备忘录何时自动生成?


106

我无法弄清楚为什么m1显然被记忆而m2不在以下内容:

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

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

m1 10000000在第一次调用时大约需要1.5秒,而在随后的调用中则花费一小部分(大概是缓存列表),而m2 10000000总是花费相同的时间(每次调用都重建列表)。知道发生了什么吗?关于GHC是否以及何时记忆功能有任何经验法则吗?谢谢。

Answers:


112

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票证。)

鉴于此,您可以看到in m1'中的表达式filter odd [1..]不包含在任何lambda-expression中,因此,该表达式每次程序运行仅计算一次,而in中m2'filter odd [1..]则将在每次输入lambda-expression时计算一次,即,在的每次通话中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])

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


1
无论如何,评估(过滤器奇数[1 ..])表达式的成本接近于零-毕竟它是惰性列表,因此,当实际评估列表时,实际成本在(x !! 10000000)应用程序中。此外,至少在以下测试中,用-O2和-O1(在我的ghc 6.12.3中)似乎仅对m1和m2进行了一次评估:(test = m1 10000000 seqm1 10000000)。但是,如果未指定优化标志,则有所不同。顺便提一下,“ f”的两个变体的最大驻留时间均为5356字节,而与优化无关(使用-O2时总分配较少)。
Ed'ka 2010年

1
@ Ed'ka:尝试使用上述定义的测试程序fmain = interact $ unlines . (show . map f . read) . lines; 有无编译-O2; 然后echo 1 | ./main。如果编写类似的测试main = print (f 5),则y可以在使用时对其进行垃圾回收,并且两者之间没有区别f
Reid Barton

嗯,那map (show . f . read)当然应该是。现在,我已经下载了GHC 6.12.3,看到的结果与GHC 6.12.1中的结果相同。是的,您对GHC 的原始m1m2版本是正确的:在启用了优化的情况下执行这种提升的GHC版本将转换m2m1
Reid Barton 2010年

是的,现在我看到了区别(-O2肯定更慢)。谢谢你的这个例子!
Ed'ka 2010年

29

m1仅计算一次,因为它是一个常数适用形式,而m2不是CAF,因此每次评估都要计算一次。

请参阅CAF上的GHC Wiki:http//www.haskell.org/haskellwiki/Constant_applicative_form


1
解释“ m1仅计算一次,因为它是常数应用形式”对我来说没有意义。因为大概m1和m2都是顶级变量,所以我认为这些函数仅计算一次,无论它们是否为CAF。区别在于,列表[1 ..]是在程序执行期间仅计算一次,还是针对该功能的每个应用程序计算一次,但是与CAF相关吗?
伊藤刚(Tsuyoshi Ito)2010年

1
在链接的页面上:“ CAF ...可以编译为所有用途共享的图形,也可以编译为某些共享的代码,这些代码在首次评估时会被某些图形覆盖”。由于m1是CAF,因此第二个适用,并且filter odd [1..](不只是[1..]!)仅计算一次。GHC还可能注意到m2指向filter odd [1..],并放置一个与相同的thunk的链接m1,但这不是一个好主意:在某些情况下,它可能导致大量内存泄漏。
Alexey Romanov

@Alexey:感谢您对修正[1..]filter odd [1..]。对于其余的,我仍然不服气。如果我没记错的话,那么CAF仅在我们要争辩说编译器可以用全局thunk 替换filter odd [1..]in 时才有意义m2(该thunk 可能与in中使用的thunk相同m1)。但在提问者的情况,编译器并没有这样做“优化”,而我不能看到其相关的问题。
伊藤刚(Tsuyoshi Ito)2010年

2
这是相关的,它可以取代它 m1,它也。
Alexey Romanov

13

两种形式之间有一个关键的区别:单态性限制适用于m1而不适用于m2,因为m2明确给出了参数。因此,m2的类型是常规的,而m1的类型是特定的。为其分配的类型为:

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

大多数Haskell编译器和解释器(我实际上都知道所有这些)并不记忆多态结构,因此,每次调用m2时都会重新创建m2的内部列表,而不会调用m1的内部列表。


1
在GHCi中处理这些问题似乎还取决于浮动转换(GHCi中未使用的GHC优化过程之一)。当然,当编译这些简单函数时,优化器能够使它们始终具有相同的行为(根据我仍然运行的某些标准测试,这些函数在单独的模块中并用NOINLINE编译指示)。大概是因为列表生成和索引无论如何都会融合到一个超级紧密的循环中。
mokus 2010年

1

我不确定,因为我本人对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

然后进行测试,我进入GHCI并写道:primes !! 1000。花费了几秒钟,但最终我得到了答案:7927。然后我打电话primes !! 1001给我,并立即得到答案。类似地,在瞬间我得到了的结果take 1000 primes,因为Haskell必须计算整个千元素列表才能返回前1001个元素。

因此,如果您可以编写不带任何参数的函数,则可能需要它。;)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.