GHC能否真的不内联地图,scanl,foldr等?
我注意到GHC手册说“对于自递归函数,断路器只能是函数本身,所以一个INLINE编译指示总是被忽略。”
这不是说每一个像map
, zip
, scan*
, 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
,该版本使用快速整数算术。
这种专业化在模块中自动发生(例如,如果fac
和f
在相同的模块中定义)。 对于跨模块专业化(例如,如果f
和fac
在不同模块中定义),请使用INLINABLE杂注标记将要专用的函数:
{-# INLINABLE fac #-}
fac :: (Eq a, Num a) => a -> a
...
有手动转换,使函数非递归。 最低功耗的技术是静态参数转换,它适用于递归函数,其参数在递归调用时不会改变(例如许多高阶函数,如map
, filter
, fold*
)。 这转变
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函数,如foldr
和foldl
。
融合技术也使得许多函数是非递归的,并且比静态参数变换更强大。 Prelude中内置的主要方法是快捷方式融合。 基本的方法是编写尽可能多的函数作为使用foldr
和/或build
非递归函数; 那么所有的递归都会在foldr
被捕获,并且有特殊的RULES来处理foldr
。
利用这种融合原则上很容易:避免手动递归,更喜欢库函数,例如foldr
, map
, filter
和这个列表中的任何函数。 特别是,用这种风格编写代码会产生“同时又快又优雅”的代码。
诸如文本和矢量的现代图书馆在幕后使用流融合。 唐·斯图尔特写了一对博客文章(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