如何判断Haskell是缓存结果还是重新计算结果?

时间:2011-02-17 18:04:28

标签: function caching haskell

我注意到有时Haskell纯函数以某种方式被缓存:如果我用相同的参数调用该函数两次,第二次立即计算结果。

  1. 为什么会这样?它是GHCI功能还是什么?
  2. 我可以依赖于此(即:我可以确定地知道是否会缓存函数值)?
  3. 我可以为某些功能调用强制或禁用此功能吗?

  4. 根据评论的要求,这是我在网上找到的一个例子:

    isPrime a = isPrimeHelper a primes
    isPrimeHelper a (p:ps)
        | p*p > a = True
        | a `mod` p == 0 = False
        | otherwise = isPrimeHelper a ps
    primes = 2 : filter isPrime [3,5..]
    

    在运行它之前,我期待它非常慢,因为它一直在访问primes的元素而没有显式地缓存它们(因此,除非这些值在某处缓存,否则它们需要重新计算很多次) 。但我错了。

    如果我在GHCI中设置+s(在每次评估后打印时间/记忆统计数据)并评估表达式primes!!10000两次,这就是我得到的:

    *Main> :set +s
    *Main> primes!!10000
    104743
    (2.10 secs, 169800904 bytes)
    *Main> primes!!10000
    104743
    (0.00 secs, 0 bytes)
    

    这意味着必须缓存至少primes !! 10000(或更好:整个primes列表,因为primes!!9999也不会花费时间)。

1 个答案:

答案 0 :(得分:31)

在您的代码中,

primes不是函数,而是一个常量,在haskellspeak中称为CAF。如果它使用了一个参数(例如,()),如果调用它两次,你会得到同一个列表的两个不同版本,但由于它是一个CAF,你两次都会得到完全相同的列表;

作为ghci顶级定义,primes永远不会变得无法访问,因此它指向的列表的头部(因此它的尾部/其余计算)从不被垃圾收集。添加参数可以防止保留该引用,然后列表将被垃圾收集,因为(!!)向下迭代以找到正确的元素,而您对(!!)的第二次调用将强制重复整个计算而不是仅仅遍历已经计算的列表。

请注意,在编译的程序中,没有像ghci那样的顶级范围,当最后一次引用它们时,事情会被垃圾收集,很可能在整个程序退出之前,CAF与否,这意味着你的第一次调用需要很长时间,第二个不会,之后,“程序的未来”不再引用CAF,CAF占用的内存将被回收。

primes package提供了一个参数(主要是,我声称)这个理由的函数,因为携带大约半TB的素数可能不是人们想要做的。

如果您想真正了解这一点,我建议您阅读STG paper。它不包括GHC的新发展,但是很好地解释了Haskell如何映射到汇编,以及通常如何通过严格性来吃掉thunk。