ghci编译器优化:两次调用具有相同参数的函数

时间:2014-05-01 12:00:40

标签: haskell optimization compiler-construction ghci

在下面的简单代码中,从二叉搜索树中删除元素的函数定义的一部分:

 deleteB x (Node n l r) | x == n = Node (leastB r) l (deleteB (leastB r) r)

编译器是否优化了代码,以便它只调用(至少B r)一次,就好像它是:

 deleteB x (Node n l r) | x == n = Node k l (deleteB k r)
                          where k = leastB r

换句话说,编译器是否能够理解由于参数r在函数deleteB的主体内没有改变,因此调用相同函数(leastB)的结果不能给出不同的结果,因此计算它两次是没用的吗?

更一般地说,如果编译器执行此优化,我将如何才能理解,以防不存在令人惊讶的stackoverflow?感谢

2 个答案:

答案 0 :(得分:7)

如果你想知道GHC"真的做了什么",你想看看"核心"输出

GHC将您的Haskell源代码(非常高级)转换为一系列低级和低级语言:

Haskell⇒核心⇒STG⇒C--⇒汇编语言⇒机器代码

几乎所有的高级优化都发生在Core中。你要问的那个基本上是"常见的子表达式消除" (CSE)。如果你考虑一下,这是一个时间/空间权衡;通过保存以前的结果,您可以使用更少的CPU时间,但也可以使用更多的RAM。如果您尝试存储的结果很小(即整数),那么这是值得的。如果结果很大(即刚刚加载的17GB文本文件的全部内容),这可能是一个非常糟糕的主意。

据我了解(⇒不太好!),GHC倾向于不做CSE。但是,如果您想确切地知道,在您的特定情况下,您希望查看您的程序实际编译到的Core。我相信你想要的开关是--ddump-prep

http://www.haskell.org/ghc/docs/7.0.2/html/users_guide/options-debugging.html

答案 1 :(得分:4)

GHC不执行此优化,因为它并不总是空间优化。

例如,考虑

n = 1000000
x = (length $ map succ [1..n], length $ map pred [1..n])

对于像Haskell这样的惰性语言,可以预期这会在恒定空间中运行。实际上,生成表达式[1..n]的列表应该一次懒惰地生成一个元素,由于succ s,它会受pred / map的影响,然后由length。 (更好的是,succpred根本没有计算,因为length不强制列表元素)。在此之后,生成的元素可以被垃圾收集,列表生成器可以生成下一个元素,依此类推。在实际实现中,人们不会期望立即对每个元素进行垃圾收集,但如果垃圾收集器是好的,那么任何时候只有一定数量的元素应该在内存中。

相比之下,“优化”代码

n = 1000000
l = [1..n]
x = (length $ map succ l, length $ map pred l)
在评估l的两个组件之前,

不允许垃圾收集x的元素。因此,虽然它只生成一次列表,但它使用O(n)个字的内存来存储完整列表。这可能会导致性能低于未经优化的代码。