19

根据 GHC文档

...GHC 仅在完全应用时内联函数,其中“完全应用”意味着应用到函数定义的 LHS 上出现的(语法上)尽可能多的参数。

给出的示例是两个语义等效的定义:

comp1 :: (b -> c) -> (a -> b) -> a -> c
{-# INLINE comp1 #-}
comp1 f g = \x -> f (g x)

comp2 :: (b -> c) -> (a -> b) -> a -> c
{-# INLINE comp2 #-}
comp2 f g x = f (g x)

我的问题:

  1. 只有在存在 INLINE pragma 的情况下,我们才会得到这种严格的行为(即 LHS 的严格语法视图,RHS 内联无优化)?

  2. 当没有给出 INLINE pragma 时,GHC 是否曾经将函数转换comp2comp1

  3. 如果不是,为什么?一般来说,编译器是否很难查看函数的语义并决定部分应用和内联的数量和位置?

  4. 如果 GHC 只是将所有函数转换为let... in具有 lambdas 且在 LHS 上没有绑定的级联表达式,会发生什么?

4

3 回答 3

8

如果在这个例子中,c它本身就是一个函数类型呢?我不清楚你的提议在这种情况下会如何运作。

无论如何,在某些情况下,您肯定不希望将函数的所有参数“拉到最前面”。例如,您可能有一些这样的代码:

foo :: [Int] -> Int -> Int -> Int
foo list = let
  -- expensive precomputation here
  bar x y = ...
  in \ x y -> bar x y

希望 foo得到部分应用,然后为生成的函数的多个应用分担昂贵的预计算工作。相反,如果您将其拉为foo list x y,您将无法共享昂贵的预计算。(我在严肃的应用程序中遇到过这种情况。)

于 2012-07-27T16:03:50.973 回答
6

这是一个很好的问题。我通读了 Glasgow Haskell Compiler Inliner论文的秘密以获取一些线索,但没有找到太多。

这是一个手动的解释。GHC 实际上有时需要comp1——comp2它称之为“eta 扩展”。有关详细信息,请参阅此线程:http ://www.haskell.org/pipermail/glasgow-haskell-users/2011-October/020979.html

也存在(或曾经)一个问题,即这种 eta 扩展可以巧妙地改变严格性。请参阅对文档的此提交(似乎不在当前文档中,因此它们尚未重建,或者已修复,不确定是哪个): http: //permalink.gmane.org/gmane .comp.lang.haskell.cvs.ghc/57721

无论如何,上面的线程都有 SPJ 解释为什么我们通常希望尽可能地朝那个方向前进。所以为了改进内联而故意转向另一个方向似乎有点愚蠢。正如秘密论文所讨论的那样,混杂地内联并不是最好的主意 - 使编译指示更像是一把钝锤,因此无论这样做是否有意义,函数都被内联可能会比整体帮助更多,更不用说增加代码膨胀,因为模块必须同时保持不同级别的 eta-shifted 函数。

无论如何,作为一个非常不是核心 GHC 开发人员的人,这对我来说似乎最有可能。

于 2012-08-02T22:23:23.990 回答
3

好吧,我想,迟到总比没有好。

comp1并且comp2不仅在语义上是等价的,甚至在句法上也是等价的。

在定义的等号的 LHS 上写参数只是语法糖,所以这两个函数是等价的:

id1 x = x
id2 = \x -> x

编辑:我意识到我并没有真正回答你的问题,所以你在这里:

  1. 当这些用INLINE编译指示注释时,GHC 会有所不同,因为 GHC 在其核心表示中存储函数的展开以及它可以展开的数量(这就是Guidance=ALWAYS_IF(arity=1,...)部分),因此它在实践中实际上很重要。

  2. 我不认为它确实如此,因为在脱糖到核心后无法区分 和 ,所有优化都在核心上运行comp1comp2因此,当 GHC 想要创建一个新的展开时,它可能会为明显的 arity(例如前导 lambda 的数量)这样做。

  3. 内联对不饱和绑定大多没有好处,见下文。示例实际上也是如此comp1:我们希望发生这种情况的原因并不是我们关心消除函数调用。相反,我们希望comp1专门f参数g,而不管x我们将专门化应用于什么具体。实际上有一个优化过程应该做这种工作,称为构造函数专门化(更多内容见下文)。在这里使用甚至完全不合适:这INLINE仍然不会专门化像.comp1 (const 5)const 5

  4. 因此,这不会有太大的改变,只要你不给每个 let-bound 的东西都撒上INLINEpragma。即使那样,这是否带来任何好处也是值得怀疑的:关键是,在没有任何进一步动机(例如,将函数专门化为具体参数)的情况下,内联不饱和调用是没有意义的,而且它只会使代码大小膨胀某个点,所以它甚至可能会使事情变慢。

结束编辑


我认为为什么不内联对绑定的不饱和调用的一个原因是它们大多不会带来任何新的优化机会。

f = \x y -> 1 + (x * y)
g = \x y -> (1 + x) * y

内联f 16yield \y -> 1 + (16*y),这并不比f 16. 相反,代码大小显着增加(这是内联的最大缺点)。

现在,如果有这样的调用g 16会产生\y -> (1 + 16) * y优化到\y -> 17 * y. 但是这些机会会被另一个优化过程、构造函数或调用模式专业化检测到。这里的见解是,1 + x如果我们知道 的值,则可以简化x。由于我们g使用文字(例如值)进行调用,因此专门g针对该特定调用站点是有益的,例如g16 = \y -> 17 *y. 无需内联g,其他呼叫站点也可能共享为g16.

这只是在仍然拥有高效代码的同时不需要进行内联的一个示例。还有许多其他优化可以与内联实现你想要的。例如,Eta-expansion 将确保调用尽可能饱和:

main = print (f 2)

f = g 1 
g x y = x + y

由于f总是用 1 个参数调用,我们可以对其进行 eta 扩展:

f eta = g 1 eta

现在调用g已饱和,可以内联。Dito for f,所以最终这减少到

main = print 3

f eta = 1 + eta
g x y = x + y

模死代码消除。

于 2017-09-12T09:42:06.077 回答