如何在haskell中使用内联的相位控制?

时间:2013-01-21 20:08:12

标签: performance haskell ghc inlining repa

文档says

  

有时您希望准确控制在GHC管道中何时打开INLINE编译指示。

我为什么要这样? (除非我也使用RULES编译指示,在这种情况下,我可能希望推迟函数的内联,以便触发相关规则。)只有在简化过程的特定阶段才能更好地内联哪种函数? / p>

2 个答案:

答案 0 :(得分:15)

正如其他人所说,你基本上回答了你自己的问题。但是我想你可能想要一个更简洁,更具体的例子,说明将相位控制与RULES / INLINE结合使用是有益的。*除了高度优化的库之外你不会看到它们通常情况很复杂,所以看到较小的案例很棒。

这是我最近使用递归方案实现的示例。我们将使用catamorphisms的概念来说明这一点。你不需要知道那些细节是什么,只是他们的特点是'折叠'运营商。 (真的,不要过多关注这里的抽象概念。这只是我所拥有的最简单的例子,你可以有一个很好的加速。)

快速介绍catamorphisms

我们从Mu,定点类型和Algebra的定义开始,这只是一个函数的奇特同义词,它解构了"值f a返回a

newtype Mu f = Mu { muF :: f (Mu f) }

type Algebra f a = f a -> a

我们现在可以定义两个运算符ffoldfbuild,它们是列表的传统foldrbuild运算符的高度通用版本:

ffold :: Functor f => Algebra f a -> Mu f -> a
ffold h = go h 
  where go g = g . fmap (go g) . muF
{-# INLINE ffold #-}

fbuild :: Functor f => (forall b. Algebra f b -> b) -> Mu f
fbuild g = g Mu
{-# INLINE fbuild #-}

粗略地说,ffold 会破坏Algebra f a定义的结构,并产生afbuild代替创建由其Algebra f a定义的结构,并生成Mu值。 Mu值对应于您正在谈论的任何递归数据类型。就像常规的foldrbuild一样:我们使用它的缺点来解构列表,我们也使用它的缺点构建一个列表。我们的想法是,我们只是概括了这些经典运算符,因此它们可以处理任何递归数据类型(如列表或树!)

最后,这两个运营商都有一条法律,它将指导我们的整体RULE

forall f g. ffold f (build g) = g f

这条规则基本上概括了森林砍伐/融合的优化 - 中间结构的去除。 (我认为所述法律的正确性证明留给了读者。通过等式推理应该相当容易。)

我们现在可以使用这两个组合器以及Mu来表示递归数据类型,例如列表。我们可以在该列表上编写操作。

data ListF a f = Nil | Cons a f
  deriving (Eq, Show, Functor)
type List a = Mu (ListF a)

instance Eq a => Eq (List a) where
  (Mu f) == (Mu g) = f == g

lengthL :: List a -> Int
lengthL = ffold g
  where g Nil = 0
        g (Cons _ f) = 1 + f
{-# INLINE lengthL #-}

我们也可以定义map函数:

mapL :: (a -> b) -> List a -> List b
mapL f = ffold g
  where g Nil = Mu Nil
        g (Cons a x) = Mu (Cons (f a) x)
{-# INLINE mapL #-}

内联FTW

我们现在有一种方法可以在我们定义的这些递归类型上编写术语。但是,如果我们写一个像

这样的术语
lengthL . mapL (+1) $ xs

然后,如果我们扩展定义,我们基本上得到两个ffold运算符的组合:

ffold g1 . ffold g2 $ ...

这意味着我们实际上正在摧毁结构,然后重建它并再次摧毁 。这真的很浪费。此外,我们可以根据mapL重新定义fbuild,因此它有望与其他功能融合。

嗯,我们已经有了法律,所以RULE是有序的。让我们编纂一下:

{-# RULES
-- Builder rule for catamorphisms
"ffold/fbuild" forall f (g :: forall b. Algebra f b -> b).
                  ffold f (fbuild g) = g f
-}

接下来,为了融合目的,我们会根据mapL重新定义fbuild

mapL2 :: (a -> b) -> List a -> List b
mapL2 f xs = fbuild (\h -> ffold (h . g) xs)
  where g Nil = Nil
        g (Cons a x) = Cons (f a) x
{-# INLINE mapL2 #-}

Aaaaa我们已经完成了,对吧?错!

乐趣和利润的阶段

问题在于内联发生时没有任何限制,这将完全搞砸了。考虑一下我们想要优化的情况:

lengthL . mapL2 (+1) $ xs

我们希望内联lengthLmapL2的定义,以便ffold/fbuild规则可以在身体之后触发。ffold f1 . fbuild g1 ... 规则。所以我们想去:

g1 f1

通过内联,然后转到:

RULE

通过我们的lengthL

嗯,这不能保证。基本上,在简化器的一个阶段,GHC可能不会内联mapLffold的定义,但它也可能内联fbuildffold的定义fbuild在他们的使用地点。这意味着RULE永远不会有机会开火,因为这个阶段已经吞噬了。所有相关标识符,并将它们内嵌到任何内容中。

观察结果是我们希望尽可能晚地内联RULEffold 。因此,我们将尝试尽可能多地为我们的RULE揭开可能的机会。如果它没有,那么身体将被内联,GHC仍然会发挥最大作用。但最终,我们希望它能延迟上线; fbuild比任何聪明的编译器优化都能为我们节省更多的效率。

因此,此处的修复是注释ffold g = ... {-# INLINE[1] ffold #-} fbuild g = ... {-# INLINE[1] fbuild #-} mapL并指定它们只应在第1阶段触发:

fbuild/ffold

现在,lengthL . mapL2和朋友们很早就会上线,但这些内容会很晚才会出现。 GHC从某个阶段编号N开始,阶段编号减少到零。第1阶段是最后阶段。也可以比第1阶段更早地内联lengthL . mapL1,但这基本上意味着你需要开始增加相位数来弥补它,或者开始确保规则总是在某些早期阶段触发。

结论

您可以在此处找到all of this and more in a gist of mine **,其中包含所有提到的定义和示例。它还附带了我们示例的标准基准:通过我们的阶段注释,当RULE触发时,GHC能够将-ddump-simpl-stats的运行时间与ffold/fbuild相比减少一半。

如果您希望自己看到这一点,可以使用RULE编译代码,并查看在编译管道中触发的INLINE规则。

最后,大多数相同的原则适用于像vector或bytestring这样的库。诀窍在于,您可能在此处具有多个内联级别,并且批次更多规则。这是因为像流/阵列融合这样的技术倾向于有效地融合循环和重用数组 - 而不是在这里,我们只是通过删除中间数据结构来进行经典的森林砍伐。取决于传统的模式'生成的代码(例如,由于矢量化的并行列表理解),以早期消除明显缺陷的方式交​​错或特定阶段优化可能是非常值得的。或者,针对RULERULE组合会产生更多RULE s的情况进行优化(因此有时会看到交错的阶段 - 这基本上会交错一个内联阶段。)出于这些原因,您还可以控制{{1}}触发的阶段。

因此,虽然带有阶段的{{1}}可以为我们节省大量的运行时间,但它们也可能需要花费大量时间才能正确运行。这就是为什么您经常只在最高性能的,高度优化的库中看到它们。

备注

  • *您最初的问题是"哪种功能可以从相位控制中受益?#34;对我来说,这听起来像是在询问"哪些功能可以从不断的子表达式消除中获益。"我不确定如何准确回答这个问题,如果可能的话!这是一个编译器领域的事情,而不是任何关于函数或程序行为的理论结果 - 甚至数学定律,而不是所有的优化'得到你期望的结果。因此,答案是有效的"您可能知道何时编写和基准测试。"

  • **您可以安全地忽略文件中的许多其他内容;它主要是一个游乐场,但也可能对你很有趣。还有其他一些例子,如自然和二叉树 - 你可能会发现尝试利用它们来利用各种其他融合机会是值得的。

答案 1 :(得分:1)

首先,我应该注意到GHC的默认行为在大多数情况下都是最佳的。除非你有问题,否则你可能最好让那些每天都在考虑haskell的聪明人大多是正确的(PS我不是不是其中一个人),但是你问...

我理解使用它的两个原因。

  1. 让程序更快地融合到最佳状态:

    Haskell将尝试每个规则重复传递,只要从另一端出来的东西严格地比它开始时更好。它总会收敛,但没有什么能说它会在宇宙热死之前这样做。在通常的情况下,它只需要一手满满的传球,但是有一些角落的情况可以在病态上变坏。这将允许您手动处理这些边缘情况。

  2. 避免收敛到当地的最低要求

    在某些情况下,应用规则A会阻止应用更好的规则B。因此BA之前很重要。默认的优化规则是精心设计的,以避免这个问题,但正如文档所说,他们也非常保守。随着您添加更多规则,您将不可避免地开始打破其他可能的优化。然后,您需要在规则链中找到一个不会发生这种情况的地方。据我所知,唯一可以判断的方法是试错。