When to evaluate strictly in Haskell?
据我所知,
1 2 3 4 5 6 | import qualified Data.Vector.Unboxed as V main :: IO () main = print $ mean (V.enumFromTo 1 (10^9)) mean :: V.Vector Double -> Double |
不同版本的平均值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | -- 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 这样的简单函数有意义吗?在这种函数中,所有的东西都应该一次性评估。
在您的示例中,爆炸模式围绕着平均值的最终计算移动,或者更确切地说是其成分:
1 2 |
但是(有一个明显的例外)不是第二个项目,折叠功能本身:
1 | f (len, acc) x = (len+1, acc+x) |
但是这个
主要的面包和黄油点是,在一个左折或积累环,所有积累的材料必须严格评估。否则,你只需要用
不过,在您的示例中,
严格性的问题出现在这里,因为您积累了不止一件事情,但需要将它们结合到一个论点中,用于您要传递给
然后你可以写
1 2 3 | 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) |
请注意,我没有用bang来标记
1 | f !(len, acc) x = (len+1, acc+x) |
但是功能
1 | f (len, acc) x = (len+1, acc+x) |
第一个参数对已经很严格了,因为您可以看到最外面的构造函数
1 2 3 | $ time ./foldstrict 5.00000000067109e8 real 0m1.495s |
而在我的机器上,你最好的办法是:
1 2 3 | $ time ./foldstrict 5.00000000067109e8 real 0m1.963s |
不是一成不变的,但当前的最佳实践是使数据结构中的所有字段都严格,但采用函数参数并延迟返回结果(累加器除外)。
净效应是只要你不碰一个返回值,什么都不会被评估。一旦您严格地需要从中得到一点信息,整个结构就会立即被评估,这将导致比在整个执行过程中被懒惰地评估更可预测的内存/CPU使用模式。
Johan Tibell的性能指南最能指出细微之处:http://johan tibell.com/files/haskell performance patterns.html(1)。请注意,最近的GHC自动执行小而严格的字段解包,而不需要注释。另请参见严格的pragma。
关于什么时候引入严格的领域:从一开始就做正确的事情,因为回顾性地使用它要困难得多。您仍然可以使用懒惰的字段,但只有当您明确需要它们时才可以使用。
注意:
注2:有专门的libs可以让您处理严格的折叠(参见foldl),或者处理流计算(管道、管道)。
更新对其原理进行了一点阐述,以便1)你知道这不仅仅是为了让橡皮鸭离开天空2)知道何时/为什么要偏离。
为什么评价严格?如问题所述,一个例子是严格的积累。这也有一些不太明显的形式——比如统计在应用程序状态下发生的某些事件。如果不存储一个严格的计数,您可以得到一个很长的
上面非正式地称为
另一种情况是并发计算,其中工作被划分为多个线程。现在,很容易遇到这样的情况:您认为您将一个计算分叉到一个单独的线程(使您的程序非常有效地并发),但后来才意识到并发线程只计算懒惰数据结构的最外层,当值为强迫。
解决这个问题的方法是使用Deepseq的
现在,如果您保持一致并意识到,使用deepseq+一次就可以了。
注:与并发评估非常相似的用例是在纯错误的可怕情况下进行单线程评估,如