什么时候在 Haskell 中严格评估?
When to evaluate strictly in Haskell?
据我所知,!
(称为刘海)用于表示应严格评估表达式。但对我来说,把它们放在哪里或者是否放它们并不是那么明显。
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
这样的简单函数有意义,所有的事情都应该一次性评估?
并非一成不变,但目前的最佳做法是使数据结构中的所有字段都严格,但延迟接受函数参数和 return 结果(累加器除外)。
最终效果是,只要您不触及一块 return 值,就不会计算任何内容。只要您严格需要它的一小部分,就会立即评估整个结构,从而导致比在整个执行过程中惰性评估它们更可预测的 memory/cpu 使用模式。
Johan Tibell 的性能指南最能指出细微之处:http://johantibell.com/files/haskell-performance-patterns.html#(1) . Note that recent GHCs perform small strict field unpacking automatically without the need to annotate. Also see the Strict pragmas。
关于何时引入严格的字段:从一开始就做,因为追溯起来更难。您仍然可以使用惰性字段,但只有在您明确需要它们时才可以。
注意:[]
是惰性的,更多地用作预期内联的控制结构,而不是用作容器。对后者使用 vector
等。
注意 2:有专门的库可以让您处理严格折叠(参见 foldl), or with streaming computations (conduit, pipes)。
更新
对基本原理进行一些详细说明,以便 1) 你知道这不仅仅是为了天上掉下来的橡皮鸭 2) 知道 when/why 偏离。
为什么要严格评价?
一种情况是严格积累,如问题中所述。这也以不太明显的形式出现 - 例如对应用程序状态中发生的某些事件进行计数。如果你不存储严格的计数,你会得到一长串 +1
thunks 建立起来,这会无缘无故地消耗大量内存(与只存储更新的计数相比)。
以上被非正式地称为 memory leak
,即使它在技术上不是泄漏(没有内存丢失,它只是保留的时间比需要的时间长)。
另一种情况是并发计算,其中工作被分配到多个线程。现在,您很容易 运行 陷入这样的情况,您认为您将计算分叉到一个单独的线程(使您的程序非常高效地并发),后来才意识到并发线程只计算惰性线程的最外层数据结构,当强制值时,大部分计算仍然发生在主线程上。
解决此问题的方法是使用 deepseq. But imagine having a final data structure layered A (B (C))
, where each layer is computed by a separate thread, deep forcing the structure before returning. Now C
is deep forced (in effect traversed in memory) three times, B
two times. If C
is a deep/large structure, this is a waste. At this point you can either add the Once trick 中的 NFData
,或者只使用深度严格的数据结构,其中对 WHNF(而不是深度 NF)进行浅层强制具有相同的效果深度强制,但是编译器会处理一次技巧,可以这么说。
现在,如果您始终如一并意识到,您可能会使用 deepseq+Once。
注意:与并发评估非常相似的一个用例是在纯错误的可怕情况下的单线程评估,例如undefined
和error
.理想情况下不使用这些,但如果使用,解决问题的方法与上面概述的方法非常相似(顺便查看 spoon 包)。
在您的示例中,爆炸模式围绕平均值的最终计算移动,或者更确切地说,它的成分:
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
。
主要的面包和奶油点是在左折叠或累积循环中,所有累积的material必须严格评估。否则你基本上只是用 foldl'
建立一个大表达式,然后当你最终用你积累的 material 做某事时要求它被折叠 - 在这里,最后计算平均值.
但是,在您的示例中,foldl'
从未给出明确的严格函数来折叠:累积的 len
和 acc
被困在常规的惰性 [=57= 中] 元组。
这里出现了严格性的问题,因为你积累了不止一种东西,但需要将它们联系在一起成为你传递给 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)
请注意,我没有用爆炸标记 (P len acc)
,因为它明显处于弱头部范式 - 您可以看到 P 并且不需要要求编译器使用 !/ 找到它seq - 因此 f
在第一个参数中是严格的。在向 f
添加严格性的情况下也是如此
f !(len, acc) x = (len+1, acc+x)
但是函数
f (len, acc) x = (len+1, acc+x)
在第一个参数中已经是严格的,对,因为你可以看到最外层的构造函数 (,)
,并且不需要严格注释(基本上只是告诉编译器找到它。)但是构造函数只是构造一个惰性元组,因此它在 len
和 acc
中不是(明确地)严格
$ time ./foldstrict
5.00000000067109e8
real 0m1.495s
而在我的机器上你最好的意思是:
$ time ./foldstrict
5.00000000067109e8
real 0m1.963s
据我所知,!
(称为刘海)用于表示应严格评估表达式。但对我来说,把它们放在哪里或者是否放它们并不是那么明显。
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
这样的简单函数有意义,所有的事情都应该一次性评估?
并非一成不变,但目前的最佳做法是使数据结构中的所有字段都严格,但延迟接受函数参数和 return 结果(累加器除外)。
最终效果是,只要您不触及一块 return 值,就不会计算任何内容。只要您严格需要它的一小部分,就会立即评估整个结构,从而导致比在整个执行过程中惰性评估它们更可预测的 memory/cpu 使用模式。
Johan Tibell 的性能指南最能指出细微之处:http://johantibell.com/files/haskell-performance-patterns.html#(1) . Note that recent GHCs perform small strict field unpacking automatically without the need to annotate. Also see the Strict pragmas。
关于何时引入严格的字段:从一开始就做,因为追溯起来更难。您仍然可以使用惰性字段,但只有在您明确需要它们时才可以。
注意:[]
是惰性的,更多地用作预期内联的控制结构,而不是用作容器。对后者使用 vector
等。
注意 2:有专门的库可以让您处理严格折叠(参见 foldl), or with streaming computations (conduit, pipes)。
更新
对基本原理进行一些详细说明,以便 1) 你知道这不仅仅是为了天上掉下来的橡皮鸭 2) 知道 when/why 偏离。
为什么要严格评价?
一种情况是严格积累,如问题中所述。这也以不太明显的形式出现 - 例如对应用程序状态中发生的某些事件进行计数。如果你不存储严格的计数,你会得到一长串 +1
thunks 建立起来,这会无缘无故地消耗大量内存(与只存储更新的计数相比)。
以上被非正式地称为 memory leak
,即使它在技术上不是泄漏(没有内存丢失,它只是保留的时间比需要的时间长)。
另一种情况是并发计算,其中工作被分配到多个线程。现在,您很容易 运行 陷入这样的情况,您认为您将计算分叉到一个单独的线程(使您的程序非常高效地并发),后来才意识到并发线程只计算惰性线程的最外层数据结构,当强制值时,大部分计算仍然发生在主线程上。
解决此问题的方法是使用 deepseq. But imagine having a final data structure layered A (B (C))
, where each layer is computed by a separate thread, deep forcing the structure before returning. Now C
is deep forced (in effect traversed in memory) three times, B
two times. If C
is a deep/large structure, this is a waste. At this point you can either add the Once trick 中的 NFData
,或者只使用深度严格的数据结构,其中对 WHNF(而不是深度 NF)进行浅层强制具有相同的效果深度强制,但是编译器会处理一次技巧,可以这么说。
现在,如果您始终如一并意识到,您可能会使用 deepseq+Once。
注意:与并发评估非常相似的一个用例是在纯错误的可怕情况下的单线程评估,例如undefined
和error
.理想情况下不使用这些,但如果使用,解决问题的方法与上面概述的方法非常相似(顺便查看 spoon 包)。
在您的示例中,爆炸模式围绕平均值的最终计算移动,或者更确切地说,它的成分:
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
。
主要的面包和奶油点是在左折叠或累积循环中,所有累积的material必须严格评估。否则你基本上只是用 foldl'
建立一个大表达式,然后当你最终用你积累的 material 做某事时要求它被折叠 - 在这里,最后计算平均值.
但是,在您的示例中,foldl'
从未给出明确的严格函数来折叠:累积的 len
和 acc
被困在常规的惰性 [=57= 中] 元组。
这里出现了严格性的问题,因为你积累了不止一种东西,但需要将它们联系在一起成为你传递给 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)
请注意,我没有用爆炸标记 (P len acc)
,因为它明显处于弱头部范式 - 您可以看到 P 并且不需要要求编译器使用 !/ 找到它seq - 因此 f
在第一个参数中是严格的。在向 f
f !(len, acc) x = (len+1, acc+x)
但是函数
f (len, acc) x = (len+1, acc+x)
在第一个参数中已经是严格的,对,因为你可以看到最外层的构造函数 (,)
,并且不需要严格注释(基本上只是告诉编译器找到它。)但是构造函数只是构造一个惰性元组,因此它在 len
和 acc
$ time ./foldstrict
5.00000000067109e8
real 0m1.495s
而在我的机器上你最好的意思是:
$ time ./foldstrict
5.00000000067109e8
real 0m1.963s