GHC能否真的不内联地图,scanl,foldr等?

我注意到GHC手册说“对于自递归函数,断路器只能是函数本身,所以一个INLINE编译指示总是被忽略。”

这不是说每一个像mapzipscan*fold*sum等常见的递归函数结构的应用程序都不能被内联吗?

当你使用它们时,你总是可以重写所有这些功能,添加适当的严格标签,或者使用像这里推荐的“流融合”这样的高级技术。

然而,这并不是所有这些都极大地限制了我们编写代码的能力,这同时又快又优雅?


的确,GHC目前不能内联递归函数。 然而:

  • GHC仍然专门提供递归功能。 例如,给出

    fac :: (Eq a, Num a) => a -> a
    fac 0 = 1
    fac n = n * fac (n-1)
    
    f :: Int -> Int
    f x = 1 + fac x
    

    GHC会发现fac用于Int -> Int类型,并为该类型生成一个专用版本的fac ,该版本使用快速整数算术。

    这种专业化在模块中自动发生(例如,如果facf在相同的模块中定义)。 对于跨模块专业化(例如,如果ffac在不同模块中定义),请使用INLINABLE杂注标记将要专用的函数:

    {-# INLINABLE fac #-}
    fac :: (Eq a, Num a) => a -> a
    ...
    
  • 有手动转换,使函数非递归。 最低功耗的技术是静态参数转换,它适用于递归函数,其参数在递归调用时不会改变(例如许多高阶函数,如mapfilterfold* )。 这转变

    map f []     = []
    map f (x:xs) = f x : map f xs
    

    map f xs0 = go xs0
      where
        go []     = []
        go (x:xs) = f x : go xs
    

    这样的电话如

     g :: [Int] -> [Int]
     g xs = map (2*) xs
    

    将有内联map和成为

     g [] = []
     g (x:xs) = 2*x : g xs
    

    此转换已应用于Prelude函数,如foldrfoldl

  • 融合技术也使得许多函数是非递归的,并且比静态参数变换更强大。 Prelude中内置的主要方法是快捷方式融合。 基本的方法是编写尽可能多的函数作为使用foldr和/或build非递归函数; 那么所有的递归都会在foldr被捕获,并且有特殊的RULES来处理foldr

    利用这种融合原则上很容易:避免手动递归,更喜欢库函数,例如foldrmapfilter和这个列表中的任何函数。 特别是,用这种风格编写代码会产生“同时又快又优雅”的代码。

  • 诸如文本和矢量的现代图书馆在幕后使用流融合。 唐·斯图尔特写了一对博客文章(1,2),在现在过时的图书馆uvector中展示了这一点,但同样的原则适用于文本和矢量。

    与快捷融合一样,利用文本和向量中的流融合原则上很容易:避免手动递归,优先选择标记为“融合”的库函数。

  • 目前正在进行改进GHC以支持递归函数内联的工作。 这属于超级编译的总标题,最近的工作似乎是由Max Bolingbroke和Neil Mitchell领导的。


  • 总之,不像你想象的那么频繁。 原因在于图书馆实施时采用了流融合等“花哨技巧”,图书馆用户无需担心。

    考虑Data.List.map 。 基本包将map定义为

    map :: (a -> b) -> [a] -> [b]
    map _ []     = []
    map f (x:xs) = f x : map f xs
    

    这张map是自我递归的,所以GHC不会内联它。

    但是, base还定义了以下重写规则:

    {-# RULES
    "map"       [~1] forall f xs.   map f xs                = build (c n -> foldr (mapFB c f) n xs)
    "mapList"   [1]  forall f.      foldr (mapFB (:) f) []  = map f
    "mapFB"     forall c f g.       mapFB (mapFB c f) g     = mapFB c (f.g) 
      #-}
    

    这将通过foldr / build融合替换map使用,然后,如果函数无法融合,则将其替换为原始map 。 因为融合是自动发生的,所以不依赖于用户意识到它。

    为证明这一切都有效,您可以检查GHC为特定输入生成的内容。 对于这个功能:

    proc1 = sum . take 10 . map (+1) . map (*2)
    
    eval1 = proc1 [1..5]
    eval2 = proc1 [1..]
    

    当使用-O2进行编译时,GHC将proc1所有内容融合为单一递归形式(如核心输出中的-ddump-simpl )。

    当然这些技术可以完成的限制。 例如, mean xs = sum xs / length xs很容易手动转换为单一折叠,并且存在可以自动执行的框架,但目前还没有已知的方法可以在标准函数和融合之间自动转换框架。 所以在这种情况下,用户需要了解编译器生成的代码的局限性。

    因此,在很多情况下,编译器足够先进,可以创建快速且优雅的代码。 知道他们何时会这样做,以及何时编译器可能会崩溃,恕我直言,这是学习如何编写高效Haskell代码的很大一部分。


    对于自递归函数,断路器只能是函数本身,所以一个INLINE编译指示总是被忽略。

    如果某些东西是递归的,为了内联它,你将不得不知道它在编译时执行了多少次。 考虑到它将是一个可变长度输入,这是不可能的。

    然而,这并不是所有这些都极大地限制了我们编写代码的能力,这同时又快又优雅?

    有一些技术虽然可以使递归调用更多,但比正常情况快得多。 例如,尾巴呼叫优化SO维基

    链接地址: http://www.djcxy.com/p/80505.html

    上一篇: Can GHC really never inline map, scanl, foldr, etc.?

    下一篇: Infinite recursion in C