Optimization GHC真的永远不会内联地图、scanl、foldr等吗。?

Optimization GHC真的永远不会内联地图、scanl、foldr等吗。?,optimization,haskell,inline,ghc,Optimization,Haskell,Inline,Ghc,我注意到上面说“对于自递归函数,循环断路器只能是函数本身,因此内联pragma总是被忽略。” 这不是说像map,zip,scan*,fold*,sum等常见递归函数构造的每个应用程序都不能内联吗 当您使用这些函数时,您可以随时重写它们,添加适当的严格性标记,或者使用建议的“流融合”之类的奇特技术 然而,所有这些不都极大地限制了我们编写既快速又优雅的代码的能力吗 对于自递归函数,循环断路器只能是函数本身,因此始终忽略内联杂注 如果某个东西是递归的,要内联它,您必须知道它在编译时执行了多少次。考虑到

我注意到上面说“对于自递归函数,循环断路器只能是函数本身,因此内联pragma总是被忽略。”

这不是说像
map
zip
scan*
fold*
sum
等常见递归函数构造的每个应用程序都不能内联吗

当您使用这些函数时,您可以随时重写它们,添加适当的严格性标记,或者使用建议的“流融合”之类的奇特技术

然而,所有这些不都极大地限制了我们编写既快速又优雅的代码的能力吗

对于自递归函数,循环断路器只能是函数本身,因此始终忽略内联杂注

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

然而,所有这些不都极大地限制了我们编写既快速又优雅的代码的能力吗


尽管有一些技术可以使递归调用比正常情况下快得多。例如,尾部调用优化(tail call optimization)

简而言之,不像您想象的那样频繁。原因是在实现这些库时采用了流融合等“奇特技术”,图书馆用户不必担心它们

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

map :: (a -> b) -> [a] -> [b]
map _ []     = []
map f (x:xs) = f x : map f xs
这个映射是自递归的,所以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) 
  #-}
这将取代使用
map
via,然后,如果无法融合该功能,则将其替换为原始
map
。因为融合是自动发生的,所以它不依赖于用户是否意识到它

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

proc1 = sum . take 10 . map (+1) . map (*2)

eval1 = proc1 [1..5]
eval2 = proc1 [1..]
当使用-O2编译时,GHC将所有
proc1
融合为一个递归形式(如
-ddump siml
的核心输出所示)

当然,这些技术所能达到的效果是有限的。例如,简单的平均函数,
mean xs=sum xs/length xs
可以轻松地手动转换为单个折叠,但是目前还没有已知的方法在标准函数和融合框架之间自动转换。因此,在这种情况下,用户确实需要意识到编译器生成的代码的局限性


因此,在许多情况下,编译器足够先进,可以创建快速而优雅的代码。了解他们何时会这样做,以及编译器何时可能崩溃,是学习如何编写高效Haskell代码的一个重要部分。

事实上,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
    ,该版本使用快速整数算法

    这种专业化在模块内自动发生(例如,如果在同一模块中定义了
    fac
    f
    )。对于跨模块专业化(例如,如果在不同模块中定义了
    f
    fac
    ),则使用以下标记待专业化功能:

  • 有一些手动转换使函数不可重复。最低功耗技术是,它适用于参数在递归调用时不改变的递归函数(例如许多高阶函数,如
    map
    filter
    fold*
    )。这种转变

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

    这样一个电话,比如

     g :: [Int] -> [Int]
     g xs = map (2*) xs
    
    将有
    map
    内联并成为

     g [] = []
     g (x:xs) = 2*x : g xs
    
    此转换已应用于前奏曲函数,如
    foldr
    foldl

  • 融合技术还可以使许多函数成为非递归函数,并且比静态参数转换更强大。编入序曲的列表的主要方法是。基本方法是尽可能多地编写使用
    foldr
    和/或
    build
    的非递归函数;然后在
    foldr
    中捕获所有递归,并且有专门的规则来处理
    foldr

    利用这种融合原则上很容易:避免手动递归,更喜欢库函数,如
    foldr
    map
    过滤器
    ,以及中的任何函数。特别是,以这种风格编写代码会产生“同时快速和优雅”的代码

  • 现代图书馆,如图书馆和后台使用。唐·斯图尔特(Don Stewart)写了两篇博客文章(,)在现在已经过时的图书馆中证明了这一点,但同样的原则也适用于文本和向量

    与快捷方式融合一样,利用文本和向量中的流融合原则上很容易:避免手动递归,更喜欢标记为“subject to fusion”的库函数

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


想象我的代码是这样的:
让{a=map f(cycle foo);b=scan g0a;c=zipwhith hab;d=takeWhile(>=0)b}插入(fst$last$zip cd,最大值d)
。理想情况下,我们应该重写它,使其成为一个简单的循环或尾部递归,只使用几个寄存器,一个用于
a
b
c
d
,当前最大值from
d
,以及
foo
的索引,bu
 g :: [Int] -> [Int]
 g xs = map (2*) xs
 g [] = []
 g (x:xs) = 2*x : g xs