何时严格评估Haskell?

时间:2016-05-16 08:21:55

标签: performance haskell optimization lazy-evaluation

据我所知,!(称为bangs)用于表示应严格评估表达式。但对于我来说,将它们放在哪里或者根本不是那么明显。

import qualified Data.Vector.Unboxed as V

main :: IO ()
main = print $ mean (V.enumFromTo 1 (10^9))

mean :: V.Vector Double -> Double

卑鄙的不同版本:

-- compiled with O2 ~ 1.14s
mean xs = acc / fromIntegral len
    where !(len, acc)    = V.foldl' f (0,0) xs :: (Int, Double)
          f (len, acc) x = (len+1, acc+x)

-- compiled with O2 ~ 1.18s
mean xs = acc / fromIntegral len
    where (!len, !acc)   = V.foldl' f (0,0) xs :: (Int, Double)
          f (len, acc) x = (len+1, acc+x)

-- compiled with O2 ~ 1.75s
mean xs = acc / fromIntegral len
    where (len, acc)      = V.foldl' f (0,0) xs :: (Int, Double)
          f !(len, acc) x = (len+1, acc+x)

-- compiled with O2 ~ 1.75s
mean xs = acc / fromIntegral len
    where (len, acc)      = V.foldl' f (0,0) xs :: (Int, Double)
          f (len, acc) x = (len+1, acc+x)

-- compiled without options ~ 6s
mean xs = acc / fromIntegral len
    where (len, acc)     = V.foldl' f (0,0) xs :: (Int, Double)
          f (len, acc) x = (len+1, acc+x)

-- compiled without options ~ 12s
mean xs = acc / fromIntegral len
    where !(len, acc)    = V.foldl' f (0,0) xs :: (Int, Double)
          f (len, acc) x = (len+1, acc+x)

其中一些是直觉上有意义的,但我希望它不是一个试错法。

  • 是否有某种方法可以检测延迟评估何时会影响性能?除了严格测试之外。

  • 仅对mean之类的简单函数有意义,其中所有内容都应该一次性评估?

2 个答案:

答案 0 :(得分:15)

在你的例子中,爆炸模式正在围绕平均值的最终计算,或者更确切地说是其成分:

where (!len, !acc)   = V.foldl' f (0,0) xs :: (Int, Double)
where !(len, acc)   = V.foldl' f (0,0) xs :: (Int, Double)

但是(有一个明显的例外)不是第二项,折叠功能本身:

       f (len, acc) x = (len+1, acc+x)

但是这个f就是行动的所在。在您的示例中,注释(len,acc)的不同方式似乎是触发编译器采用与f有关的微妙不同视图。这就是为什么一切看起来都有些神秘。要做的是直接处理f

主要的面包点是左侧折叠或累积循环中的,所有累积的材料必须严格评估。否则你基本上只是用foldl'构建一个大表达式,然后当你最终用你积累的材料做一些事情时要求它被折叠 - 这里,最后计算平均值。

在您的示例中,foldl'永远不会被赋予明确的严格函数来折叠:累积lenacc被困在常规的惰性Haskell元组中。

这里出现了严格的问题,因为你正在积累多个东西,但是需要将它们组合成一个参数,用于传递给foldl'的f操作。这是编写严格类型或记录来进行累积的典型案例;这需要一个短线

data P = P !Int !Double

然后你可以写

mean0 xs = acc / fromIntegral len
    where P len acc    = V.foldl' f (P 0 0) xs 
          f (P len acc) !x = P (len+1) (acc+x)

请注意,我没有用{b}标记(P len acc)因为它看起来很脆弱 - 你可以看到P并且不需要让编译器找到它!/ seq - 因此f在第一个参数中是严格的。在您对f

添加严格性的情况下也是如此
          f !(len, acc) x = (len+1, acc+x)

但功能

          f (len, acc) x = (len+1, acc+x)

在第一个参数中已经是严格的,因为你可以看到最外层的构造函数(,),并且不需要严格注释(这基本上只是告诉编译器找到它。)但是构造函数只构造一个惰性元组,因此lenacc

中没有(显式)严格
$ time ./foldstrict
5.00000000067109e8
real    0m1.495s

而在我的机器上,你最好的意思是:

$ time ./foldstrict
5.00000000067109e8
real    0m1.963s

答案 1 :(得分:12)

不是一成不变的,但目前的最佳做法是使数据结构中的所有字段都严格,但是接受函数参数并且懒惰地返回结果(累加器除外)。

净效应是,只要您不触及一条返回值,就不会评估任何内容。只要您严格需要它,就可以立即评估整个结构,从而导致更可预测的内存/ CPU使用模式,而不是在执行过程中对它们进行懒惰评估。

Johan Tibell的表现指南最能指出细微之处:http://johantibell.com/files/haskell-performance-patterns.html#(1)。请注意,最近的GHC会自动执行小型严格的字段解包,而无需进行注释。另请参阅Strict pragma。

关于何时引入严格的字段:从一开始就做到这一点,因为回溯一下就难以解决它。您仍然可以使用惰性字段,但仅限于您明确需要它们时。

注意:[]是惰性的,并且更多地用作预期内联的控件结构,而不是容器。对后者使用vector等。

注意2:有专门的库可让您处理严格折叠(请参阅foldl),或使用流式计算(conduitpipes)。

更新

对基本原理的一点阐述,以便1)你知道这不仅仅是来自天空的橡皮鸭2)知道什么时候/为什么要偏离。

为什么评价严格?

一个案例是严格积累,如问题中所述。这也是不那么明显的形式 - 比如在应用程序的状态下保持某些事件的计数。如果你不存储严格的计数,你可以得到一长串+1 thunk,这会消耗大量的内存,没有充分的理由(仅存储更新的计数)。

上面被非正式地称为memory leak,即使它在技术上不是泄密(没有记忆丢失,只是持有的时间超过了需要)。

另一种情况是并发计算,其中工作分为多个线程。现在,很容易遇到这样的情况,你认为你将计算分解为一个单独的线程(使你的程序非常有效地并发),后来才意识到并发线程只计算一个惰性数据结构的最外层,当强制该值时,大部分计算仍会在主线程上发生。

此解决方案路径使用deepseq中的NFData。但是想象一下,最终的数据结构是A (B (C)),其中每一层都由一个单独的线程计算,在返回之前深度强制结构。现在C被强迫(实际上在内存中遍历)三次,B两次。如果C是一个深/大结构,这是一种浪费。此时你可以添加Once trick,或者只使用一个严格的数据结构,其中对WHNF(而不是深度NF)的浅强迫具有与深度强制相同的效果,但是Once-trick是由编译器照顾,可以这么说。

现在,如果你是一致的并且意识到,你可能会使用deepseq + Once。

注意:与并发评估非常相似的用例是在纯错误的可怕情况下进行单线程评估,例如undefinederror。理想情况下不使用它们,但如果是,攻击问题的方法与上面概述的方式非常相似(请参阅spoon包的方式)。