11

我有一个关于在 Haskell 中使用数组实现缓存(记忆)的问题。以下模式有效:

f = (fA !)
  where fA = listArray...

但这不是(程序的速度表明每次调用或其他东西都会重新创建数组):

f n = (fA ! n)
  where fA = listArray...

在 where 子句之外(在“全局范围”中)定义 fA 也适用于任一模式。

我希望有人能指出我对上述两种模式之间区别的技术解释。

请注意,我使用的是最新的 GHC,我不确定这只是编译器的特性还是语言本身的一部分。

编辑: !用于数组访问,所以 fA !5 表示 C++ 语法中的 fA[5]。我对 Haskell 的理解是 (fA !) n 将与 (fA ! n) 相同......而且对我来说写“fn = fA ! n”(不带括号)会更传统。无论如何,无论我如何括号,我都会得到相同的行为。

4

3 回答 3

8

Haskell 标准未指定行为差异。它所要说的是功能是相同的(在给定相同输入的情况下将产生相同的输出)。

然而,在这种情况下,有一种简单的方法可以预测大多数编译器所遵循的时间和内存性能。我再次强调这不是必需的,只是大多数编译器都会这样做。

首先将您的两个示例重写为纯 lambda 表达式,扩展该部分:

f = let fA = listArray ... in \n -> fA ! n
f' = \n -> let fA = listArray ... in fA ! n

编译器使用 let 绑定来指示共享。保证是在给定的环境中(一组局部变量,lambda body,类似的东西),不带参数的 let 绑定的右侧最多会被评估一次。前者 fA 的环境是整个程序,因为它不在任何 lambda 下,而后者的环境较小,因为它在 lambda 下。

这意味着在后者中,可以对每个不同的 n 评估一次 fA,而在前者中这是被禁止的。

即使使用多参数函数,我们也可以看到这种模式的效果:

g x y = (a ! y) where a = [ x ^ y' | y' <- [0..] ]
g' x = (\y -> a ! y) where a = [ x ^ y' | y' <- [0..] ]

然后在:

let k = g 2 in k 100 + k 100

我们可能会多次计算 2^100,但在:

let k = g' 2 in k 100 + k 100

我们只会计算一次。

如果您正在使用 memoization,我建议在 Hackage 上使用 data-memocombinators,它是一个包含不同形状的 memo 表的库,因此您不必自己动手。

于 2008-11-21T12:01:39.350 回答
5

找出发生了什么的最好方法是告诉编译器输出它的中间表示-v4。输出内容繁多且有点难以阅读,但应该可以让您准确找出生成代码的差异,以及编译器是如何到达那里的。

您可能会注意到fA在您的第一个示例中它被移到了函数之外(到“全局范围”)。在您的第二个示例中,它可能不是(这意味着它将在每次调用时重新创建)。

它没有被移出函数的一个可能原因是编译器认为它取决于n. n在您的工作示例中,没有fA依赖。

但我认为编译器fA在第二个示例中避免移动到外部的原因是因为它试图避免空间泄漏。考虑一下如果fA不是您的数组,而是一个无限列表(您在其上使用了!!运算符)会发生什么。想象一下,您曾经用大数字(例如f 10000)调用它,后来只用小数字(f 2, f 3, f 12...)调用它。先前调用的 10000 个元素仍在内存中,浪费空间。因此,为避免这种情况,fA每次调用函数时编译器都会再次创建。

避免空间泄漏可能不会在您的第一个示例中发生,因为在这种情况下f实际上只调用一次,返回一个闭包(我们现在处于纯函数和命令世界的前沿,所以事情变得更加微妙) . 这个闭包替换了原来的函数,它永远不会被再次调用,所以fA只被调用一次(因此优化器可以随意将它移到函数之外)。在您的第二个示例中,f不会被闭包替换(因为它的值取决于参数),因此将再次被调用。

如果您想尝试了解更多内容(这将有助于阅读-v4输出),您可以查看Spineless Tagless G-Machine论文(citeseer 链接)。

至于你的最后一个问题,我认为这是编译器的特性(但我可能是错的)。但是,如果所有编译器都做同样的事情,即使它不是语言的一部分,我也不会感到惊讶。

于 2008-11-21T00:26:28.237 回答
0

很酷,感谢您的回答,这对您有很大帮助,我一定会查看 Hackage 上的 data-memocombinators。来自 C++-heavy 背景,我一直在努力理解 Haskell 将用给定程序做什么(主要是在复杂性方面),这些教程似乎没有涉及。

于 2008-11-21T19:02:21.810 回答