其他答案已经讨论了 的含义及其seq
与 的关系pseq
。但是,对于's 警告的确切含义,似乎有些混乱。seq
确实,从技术上讲,a `seq` b
不保证 a
会在之前进行评估b
。这似乎令人不安:如果是这样的话,它怎么可能达到它的目的?让我们考虑一下 Jon 在他们的回答中给出的例子:
foldl' :: (a -> b -> a) -> a -> [b] -> a
foldl' f acc [] = acc
foldl' f acc (x : xs)
= acc' `seq` foldl' f acc' xs
where
acc' = f acc x
当然,我们关心的acc'
是在递归调用之前被评估。如果不是,那么整个目的foldl'
就失去了!那么为什么不在pseq
这里使用呢?真的seq
那么有用吗?
幸运的是,情况实际上并没有那么可怕。seq
真的是这里的正确选择。GHC 永远不会真正选择编译,以便foldl'
在评估之前评估递归调用acc'
,因此我们想要的行为被保留。seq
和之间的区别在于pseq
优化器在认为它有特别好的理由时必须做出不同决定的灵活性。
理解seq
和pseq
严格
要理解这意味着什么,我们必须学会像 GHC 优化器一样思考。seq
在实践中,和之间的唯一具体区别pseq
是它们如何影响严格度分析器:
seq
在它的两个论点中都被认为是严格的。也就是说,在像这样的函数定义中
f a b c = (a `seq` b) + c
f
将在其所有三个论点中被认为是严格的。
pseq
就像seq
,但它只在第一个参数中被认为是严格的,而不是第二个参数。这意味着在函数定义中
g a b c = (a `pseq` b) + c
g
a
在and中会被认为是严格的c
,但不是 b
。
这是什么意思?好吧,让我们首先定义一个函数“在其一个参数上严格”的含义。这个想法是,如果一个函数在它的一个参数中是严格的,那么对该函数的调用保证会评估该参数。这有几个含义:
假设我们有一个foo :: Int -> Int
参数严格的函数,并且假设我们有一个foo
看起来像这样的调用:
foo (x + y)
一个朴素的 Haskell 编译器会为表达式构造一个 thunkx + y
并将生成的 thunk 传递给foo
. 但是我们知道,评估必然会foo
迫使这种重击,所以我们并没有从这种懒惰中获得任何好处。最好立即评估,然后将结果传递给以保存不必要的 thunk 分配。x + y
foo
由于我们知道没有任何理由将 thunk 传递给foo
,因此我们有机会进行额外的优化。例如,优化器可以选择在内部重写foo
以采用 unboxedInt#
而不是 ,Int
不仅避免 thunk 构造,x + y
而且避免完全装箱结果值。这允许将结果x + y
直接传递到堆栈上,而不是堆上。
如您所见,严格性分析对于制作高效的 Haskell 编译器至关重要,因为它允许编译器就如何编译函数调用等做出更明智的决定。出于这个原因,我们通常希望严格性分析能够找到尽可能多的机会来热切地评估事物,让我们节省无用的堆分配。
考虑到这一点,让我们回到上面f
的g
例子。让我们考虑一下我们直观地期望这些函数具有什么样的严格性:
回想一下, 的主体f
是(a `seq` b) + c
。即使我们seq
完全忽略 的特殊属性,我们也知道它最终会评估为它的第二个参数。这意味着至少f
应该像它的身体一样严格(完全未使用)。b + c
a
我们知道,评估b + c
必须从根本上同时评估b
和c
,因此f
必须至少对b
和都严格c
。是否严格a
是更有趣的问题。如果seq
was 实际上只是flip const
,则不会, asa
不会被使用,但当然整个重点是seq
引入人为的严格性,所以实际上f
在 中也被认为是严格的a
。
令人高兴的是,f
我上面提到的严格性完全符合我们对它应该具有什么严格性的直觉。f
正如我们所期望的那样,它的所有论点都很严格。
直觉上,以上所有的推理f
都应该适用于g
。唯一的区别是替换seq
with pseq
,我们知道这pseq
提供了比do 更强大的评估顺序保证seq
,所以我们希望g
至少与f
... 一样严格,也就是说,在所有参数中也严格。
然而,值得注意的是,这不是GHC 推断的严格性g
。GHC 考虑g
严格 ina
和c
,但不考虑b
,即使根据我们上面对严格性的定义,在 :g
中很明显是严格的b
:b
必须对其求值g
才能产生结果!正如我们将要看到的,正是这种差异使人pseq
如此神奇,以及为什么它通常是一个坏主意。
严格的含义
我们现在已经看到,这seq
会导致我们期望的严格性,而pseq
不会,但这意味着什么并不是很明显。为了说明,考虑一个可能的调用站点,其中f
使用了:
f a (b + 1) c
我们知道它f
的所有参数都是严格的,所以根据我们上面使用的相同推理,GHC 应该b + 1
急切地评估并将其结果传递给f
,避免重击。
乍一看,这似乎一切都很好,但是等等:如果a
是重击怎么办?尽管f
in 也是严格的a
,但它只是一个简单的变量——也许它是从其他地方作为参数传入的——a
如果f
要强制它自己,GHC 没有理由在这里急切地强制它。我们强制的唯一原因b + 1
是避免创建一个新的thunk,但我们除了强制a
在调用站点上已经创建之外什么都没有。这意味着a
实际上可能作为未评估的重击传递。
这是一个问题,因为在 的正文中f
,我们写道a `seq` b
,要求在之前a
进行评估。但是按照我们上面的推理,GHC 只是先进行了评估!如果我们真的,真的需要确保在 is之后才对is 进行评估,那么这种急切的评估是不允许的。 b
b
b
a
当然,这正是为什么pseq
在第二个参数中被认为是惰性的,尽管实际上并非如此。如果我们用 替换f
,g
那么 GHC 会乖乖地分配一个新的 thunk forb + 1
并将其传递到堆上,确保不会过早地对其进行评估。这当然意味着更多的堆分配,没有拆箱,并且(最糟糕的是)没有在调用链上进一步传播严格性信息,从而产生潜在的级联悲观。但是,嘿,这就是我们所要求的:b
不惜一切代价避免过早评估!
希望这能说明为什么pseq
是诱人的,但最终会适得其反,除非你真的知道自己在做什么。当然,您可以保证您正在寻找的评估......但是要付出什么代价?
外卖
希望以上解释清楚地说明了两者seq
的pseq
优缺点:
我们如何知道选择哪些权衡?虽然我们现在可能理解为什么 seq
有时无法在第二个参数之前评估它的第一个参数,但我们没有更多理由相信这是一件可以发生的事情。
为了缓解你的恐惧,让我们退后一步,想想这里到底发生了什么。请注意,GHC从未以之前无法评估的方式实际编译a `seq` b
表达式本身。给定一个像这样的表达式,GHC 永远不会偷偷在你背后捅你一刀,在评估之前先评估。相反,它的作用要微妙得多:它可能会间接导致和在评估整体表达式之前被单独评估,因为严格分析器会注意到整体表达式在 和 中仍然是严格的。a
b
a `seq` (b + c)
b + c
a
b
c
b + c
b
c
所有这些如何组合在一起非常棘手,它可能会让你头晕目眩,所以也许你根本不会觉得上一段那么舒缓。但是为了更具体一点,让我们回到foldl'
这个答案开头的例子。回想一下,它包含这样的表达式:
acc' `seq` foldl' f acc' xs
为了避免 thunk 爆炸,我们需要 acc'
在递归调用之前进行评估foldl'
。但鉴于上述推理,它仍然会永远如此!seq
这里相对于的差异pseq
再次仅与严格性分析有关:它允许 GHC 推断此表达式在 和 中也是严格的f
,xs
而不仅仅是acc'
,在这种情况下实际上并没有太大变化:
所以,如果这里实际上没有任何区别,为什么不使用pseq
?好吧,假设foldl'
在调用站点被内联了一些有限次数,因为它的第二个参数的形状可能是部分已知的。暴露的严格性信息seq
可能会在调用站点暴露几个额外的优化,从而导致一系列有利的优化。如果pseq
使用了,这些优化会被掩盖,GHC 会产生更糟糕的代码。
因此,这里真正的收获是,即使有时seq
可能不会在第二个参数之前评估它的第一个参数,但这仅在技术上是正确的,它发生的方式是微妙的,而且它不太可能破坏你的程序。这应该不足为奇:GHC 的作者希望程序员在这种情况下使用的工具是什么,所以让他们做错事是相当粗鲁的!是这项工作的惯用工具,不是,所以使用.seq
seq
pseq
seq
那你什么时候用pseq
呢?只有当您真的非常关心一个非常具体的评估顺序时,这通常只发生在以下两个原因之一:您正在使用par
基于 - 的并行性,或者您正在使用unsafePerformIO
并关心副作用的顺序。如果你没有做这些事情中的任何一个,那么不要使用pseq
. 如果您只关心用例,例如foldl'
,您只想避免不必要的 thunk 堆积,请使用seq
. 这就是它的用途。