首先,公共子表达式消除(CSE)意味着如果表达式出现在多个位置,则重新排列代码,以便仅计算该表达式的值一次。例如:
foo x = (bar x) * (bar x)
可能会转变为
foo x = let x' = bar x in x' * x'
因此,bar函数只被调用一次。 (如果bar是一个特别昂贵的功能,这可能会节省很多工作。) GHC并不像您期望的那样经常执行CSE。问题是,执行CSE会影响程序的严格性/懒惰性。所以GHC确实做了CSE,但仅在特定情况下---参见GHC手册。 (第??)
长话短说:"如果您关心CSE,请手工完成。"
我想知道在什么情况下CSE"会影响"程序的严格/懒惰以及可能产生的影响。
答案 0 :(得分:13)
天真的CSE规则是
e'[e, e] ~> let x = e in e'[x, x].
也就是说,只要子表达式e
在表达式e'
中出现两次,我们就会使用let-binding来计算e
一次。然而,这导致一些微不足道的空间泄漏。例如
sum [1..n] + prod [1..n]
通常在惰性函数式编程语言中使用O(1)
空间,如Haskell(如sum
和prod
将尾递归并等等等等),但会成为{{1}当天真的CSE规则颁布时。当O(n)
很高时,这对于程序来说可能很糟糕!
然后,方法是使此规则更具体,将其限制为我们知道不会出现问题的一小组案例。我们可以从更具体的列举天真规则的问题开始,这将形成我们开发更好的CSE的一系列优先事项:
n
中出现e
两次可能相距,导致e'
绑定的生命周期很长。
let-binding必须始终分配一个以前可能没有的闭包。
这可以创建一个未绑定数量的闭包。
有些情况下,关闭可能永远不会解除分配。
let x = e
这是一个更保守的规则,但更安全。在这里我们认识到let x = e in e'[e] ~> let x = e in e'[x]
出现两次,但第一次出现语法上支配第二个表达式,这意味着程序员已经引入了一个let-binding。我们可以安全地重复使用let-binding,并将e
的第二次出现替换为e
。没有分配新的闭包。
句法统治的另一个例子:
x
而另一个:
case e of { x -> e'[e] } ~> case e of { x -> e'[x] }
这些规则都利用了程序中的现有结构,以确保在转换之前和之后空间使用的动力学保持不变。它们比最初的CSE更保守,但它们也更安全。
有关懒惰FPL中CSE的完整讨论,请阅读Chitil's (very accessible) 1997 paper。有关CSE如何在生产编译器中工作的完整处理,请参阅GHC's CSE.hs module,由于GHC编写长脚注的传统,因此非常详细地记录了这些内容。该模块中的注释与代码比率不在图表中。还要注意该文件的年龄(1993)!