GHC Haskell 中啥时候自动记忆?

Posted

技术标签:

【中文标题】GHC Haskell 中啥时候自动记忆?【英文标题】:When is memoization automatic in GHC Haskell?GHC Haskell 中什么时候自动记忆? 【发布时间】:2010-10-16 22:06:46 【问题描述】:

我不明白为什么 m1 显然被记住了,而 m2 不在下面:

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

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

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

【问题讨论】:

【参考方案1】:

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' 中,@987654329 @ 将在每次输入 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 的内存中读取大约 1 GB,而原始版本将在恒定空间中运行并适合处理器的缓存。事实上,在 GHC 6.12.1 下,函数f 在编译 优化时几乎是使用-O2 编译时的两倍。

【讨论】:

评估 (filter odd [1..]) 表达式的成本无论如何都接近于零 - 毕竟它是惰性列表,所以真正的成本在 (x !! 10000000) 应用程序中时list 实际上是被评估的。此外,至少在以下测试中,m1 和 m2 似乎只用 -O2 和 -O1 (在我的 ghc 6.12.3 上)评估一次:(test = m1 10000000 seq m1 10000000)。但是,当没有指定优化标志时会有所不同。顺便说一下,无论优化如何,“f”的两个变体的最大驻留时间均为 5356 字节(使用 -O2 时总分配较少)。 @Ed'ka:试试这个测试程序,上面定义fmain = interact $ unlines . (show . map f . read) . lines;编译有或没有-O2;然后echo 1 | ./main。如果你写像main = print (f 5)这样的测试,那么y可以在使用时被垃圾回收,这两个fs没有区别。 呃,当然应该是map (show . f . read)。现在我已经下载了 GHC 6.12.3,我看到了与 GHC 6.12.1 相同的结果。是的,您对原始 m1m2 的看法是正确的:在启用优化的情况下执行这种提升的 GHC 版本会将 m2 转换为 m1 是的,现在我看到了区别(-O2 肯定更慢)。感谢您提供此示例!【参考方案2】:

m1 只计算一次,因为它是一个常量应用形式,而 m2 不是 CAF,因此每次计算都会计算。

请参阅有关 CAF 的 GHC wiki:http://www.haskell.org/haskellwiki/Constant_applicative_form

【讨论】:

解释“m1 只计算一次,因为它是一个常量应用形式”对我来说没有意义。因为大概 m1 和 m2 都是***变量,所以我认为这些 函数 只计算一次,无论它们是否是 CAF。区别在于列表[1 ..]是在程序执行期间只计算一次还是每次应用函数计算一次,但它与CAF有关吗? 来自链接页面:“CAF ...可以编译为将由所有用户共享的图形,也可以编译为某些共享代码,该代码将在第一次使用某些图形时覆盖自身它被评估”。由于m1 是一个CAF,第二个适用并且filter odd [1..](不仅仅是[1..]!)只计算一次。 GHC 还可以注意到m2 指的是filter odd [1..],并放置指向m1 中使用的相同thunk 的链接,但这是一个坏主意:在某些情况下可能会导致大量内存泄漏。 @Alexey:感谢您对[1..]filter odd [1..] 的更正。其余的,我仍然不相信。如果我没记错的话,CAF 仅在我们想争辩说编译器 可以 用全局 thunk 替换 m2 中的 filter odd [1..] 时才相关(甚至可能与之前的 thunk 相同)用于m1)。但在提问者的情况下,编译器没有没有做那个“优化”,我看不出它与问题的相关性。 相关的是它可以替换它in m1,确实如此。【参考方案3】:

这两种形式有一个关键的区别:单态限制适用于 m1 但不适用于 m2,因为 m2 已明确给出参数。所以 m2 的类型是通用的,但 m1 的类型是特定的。它们被分配的类型是:

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

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

【讨论】:

在 GHCi 中使用这些似乎也取决于 let-floating 转换(GHC 的优化通道之一,未在 GHCi 中使用)。当然,在编译这些简单函数时,优化器无论如何都能够使它们的行为相同(根据我无论如何都运行的一些标准测试,这些函数在一个单独的模块中并用 NOINLINE 编译指示标记)。这大概是因为列表生成和索引无论如何都会融合到一个超级紧密的循环中。【参考方案4】:

我不确定,因为我自己对 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 个元素。

因此,如果您可以编写不带参数的函数,那么您可能想要它。 ;)

【讨论】:

以上是关于GHC Haskell 中啥时候自动记忆?的主要内容,如果未能解决你的问题,请参考以下文章

Haskell 的 GHC 未安装

Ghc:部分编译 Haskell 代码?

Haskell、GHC、win32、开罗

在 Opensuse 42.3 上为 haskell 堆栈设置 ghc-8.2.1 时出现 ghc 完整性检查错误

GHC - Haskell 中的中缀声明

使用 GHC API 评估 Haskell 语句/表达式