什么是Haskell的融合?

时间:2016-08-11 20:17:05

标签: haskell optimization ghc stream-fusion

我一次又一次地注意到Haskell文档中的以下内容: (例如在Data.Text中):

  

受融合影响

什么是 fusion 以及如何使用它?

1 个答案:

答案 0 :(得分:51)

通常,融合是指旨在消除中间数据结构的转换。您融合函数调用会导致浪费的内存分配更高效。这实际上是IMO Haskell最纯粹的应用之一。你几乎不需要做任何事情来获得它,它通过GHC编译器免费提供。

Haskell是纯粹的

因为Haskell是纯粹的,我们得到这个名为referential transparency的东西,它(来自链接)意味着&#34;表达式总是在任何上下文中评估相同的结果&#34; 1 < / SUP>。这意味着我可以在不改变程序实际输出内容的情况下进行非常一般的程序级​​操作。例如,即使不知道xyzw是什么,我也总是知道

 ((x ++ y) ++ z) ++ w

会评估与

相同的内容
 x ++ (y ++ (z ++ w))

然而第二个实际上将涉及更少的内存分配(因为x ++ y需要重新分配输出列表的整个前缀)。

重写规则

事实上,我们可以做很多这样的优化,并且,因为Haskell是纯粹的,我们基本上只需移动整个表达式(替换xy,{对于在上面的示例中评估为列表的实际列表或表达式,{1}}或z不会更改任何内容。这成为一个非常机械的过程。

此外,事实证明,你可以为更高阶函数(Theorems for free!)提出许多等价。例如,

w

无论map f (map g xs) = map (f . g) xs fg是什么(双方在语义上相等)。然而,虽然这个等式的两边产生相同的值输出,但左侧的效率总是更差:它最终为中间列表xs分配空间,立即丢弃。我们想告诉编译器,只要遇到map g xs之类的内容,就用map f (map g xs)替换它。而且,对于GHC,这是通过rewrite rules

map (f . g) xs

{-# RULES "map/map" forall f g xs. map f (map g xs) = map (f.g) xs #-} fg可以与任何表达式匹配,而不仅仅是变量(因此xs之类的内容会转换为map (+1) (map (*2) ([1,2] ++ [3,4]))。 (There doesn't appear to be a good way to search for rewrite rules,所以我编译了一个listThis paper解释了GHC重写规则的动机和工作原理。

那么GHC如何优化map ((+1) . (*2)) ([1,2] ++ [3,4])

实际上,并不完全。上面的内容是short-cut fusion。这个名称意味着缺点:它没有太大的扩展性并且很难调试。您最终必须为相同的常用功能的所有安排编写大量的临时规则。然后,您希望重复应用重写规则可以很好地简化表达式。

事实证明,在某些情况下,我们可以通过组织我们的重写规则来做得更好,这样我们就可以构建一些中间正常形式,然后制定针对该中间形式的规则。通过这种方式,我们开始获得热销&#34;重写规则的路径。

这些系统中最先进的可能是stream fusion针对同源序列(基本上像列表这样的懒惰序列)。查看this thesisthis paper(实际上几乎是vector包的实现方式)。例如,在map中,您的代码首先会转换为涉及vectorStream s的中间形式,并以该形式进行优化,然后转换回矢量。

并且...... Bundle

Data.Text使用流融合来最小化发生的内存分配数量(我认为这对于严格的变体尤为重要)。如果您查看source,则会看到功能&#34;受融合&#34;实际上大部分操纵Streams(它们是一般形式Data.Text)并且有一堆unstream . (stuff manipulating stream) . stream个编译指示用于转换RULES s。最后,这些函数的任何组合都应该融合,以便只需要进行一次分配。

那么,我需要为日常编码带走什么?

了解代码何时融合的唯一真正方法是充分了解所涉及的重写规则,并充分了解GHC的工作原理。也就是说,你有应该做的一件事:尽可能尝试使用非递归的高阶函数,因为这些函数可以(至少现在,但一般来说总是会更多)稠合的。

并发症

因为Haskell中的融合是通过重复应用重写规则而发生的,所以足以说服自己每个重写规则的正确性,以便知道整个&#34;融合&#34;程序与原始程序的功能相同。除了与程序终止有关的边缘情况。例如,有人可能会认为

Stream

但显然不是这样,因为 reverse (reverse xs) = xs 将不会终止head $ reverse (reverse [1..])More information from the Haskell Wiki

1 只有在这些上下文中表达式保持相同的类型时,这才是真的。