我正在阅读Simon Peyton Jones等人撰写的论文。名为“Playing by the Rules: Rewriting as a practical optimization technique in GHC”。在第二部分,即他们写的“基本思想”:
考虑熟悉的
map
函数,该函数将函数应用于列表的每个元素。写在Haskell中,map
看起来像这样:
map f [] = []
map f (x:xs) = f x : map f xs
现在假设编译器遇到以下
的调用map
:
map f (map g xs)
我们知道这个表达式等同于
map (f . g) xs
(其中“。”是函数组合),我们知道后一个表达式比前者更有效,因为没有中间列表。但是编译器没有这样的知识。
一个可能的反驳是编译器应该更聪明---但程序员总是会知道编译器无法弄清楚的事情。另一个建议是:允许程序员将这些知识直接传递给编译器。这是我们在这里探讨的方向。
我的问题是,为什么我们不能让编译器变得更聪明?作者说“但程序员总是会知道编译器无法弄清楚的东西”。但是,这不是一个有效的答案,因为编译器确实可以确定map f (map g xs)
等同于map (f . g) xs
,这是如何:
map f (map g xs)
map g xs
与map f [] = []
统一。
因此map g [] = []
。
map f (map g []) = map f []
。
map f []
与map f [] = []
结合使用。
因此map f (map g []) = []
。
map g xs
与map f (x:xs) = f x : map f xs
统一。
因此map g (x:xs) = g x : map g xs
。
map f (map g (x:xs)) = map f (g x : map g xs)
。
map f (g x : map g xs)
与map f (x:xs) = f x : map f xs
结合使用。
因此map f (map g (x:xs)) = f (g x) : map f (map g xs)
。
因此我们现在有了规则:
map f (map g []) = []
map f (map g (x:xs)) = f (g x) : map f (map g xs)
正如您所看到的,f (g x)
只是(f . g)
,而map f (map g xs)
是递归调用的。这正是map (f . g) xs
的定义。这种自动转换的算法似乎很简单。那么为什么不实现这个而不是重写规则呢?
答案 0 :(得分:34)
积极的内联可以得出许多重写规则是短手的等同。 不同之处在于内联是“盲目的”,因此您事先并不知道结果是好还是坏,或者即使它会终止。
然而,根据有关该计划的更高层次的事实,重写规则可以完成非显而易见的事情。将重写规则想象为向优化器添加新公理。通过添加这些,您可以应用更丰富的规则集,从而使复杂的优化更容易应用。
例如, Stream fusion会更改数据类型表示。这不能通过内联来表达,因为它涉及表示类型更改(我们根据Stream
ADT重新定义优化问题)。在重写规则中易于陈述,单独内联是不可能的。
答案 1 :(得分:7)
在我的学生Johannes Bader的学士论文中调查了这方面的一些事情:Finding Equations in Functional Programs(PDF file)。
在某种程度上,这肯定是可能的,但
然而,在其他转换(例如内联和各种融合形式)之后进行清理是有用的。
答案 2 :(得分:1)
这可以被视为在特定情况下平衡期望与在一般情况下平衡它们之间的平衡。这种平衡会产生一些有趣的情况,你可以知道如何更快地制作某些东西,但如果你不这样做,那么对于一般的语言来说这样更好。
在您给出的结构中的地图的特定情况下,计算机可以找到优化。但是,相关结构呢?如果函数没有映射怎么办?如果有一个额外的间接层,例如返回map的函数,该怎么办?在这些情况下,编译器无法轻松优化。这是一般情况问题。
如果优化特殊情况,如果出现两种结果之一
鉴于开发人员需要在一般情况下考虑这种优化,我们希望看到开发人员在简单的情况下进行这些优化,首先减少对优化的需求!
现在,如果事实证明您感兴趣的特定情况占Haskell中世界代码库的2%,那么应用特殊情况优化会有更强大的理由。