在向Haskell新手解释foldr
时,规范定义是
foldr :: (a -> b -> b) -> b -> [a] -> b
foldr _ z [] = z
foldr f z (x:xs) = f x (foldr f z xs)
但是在GHC.Base中,foldr
被定义为
foldr k z = go
where
go [] = z
go (y:ys) = y `k` go ys
似乎这个定义是对速度的优化,但我不明白为什么使用辅助函数go
会使速度更快。源评论(see here)提到内联,但我也不知道这个定义如何改进内联。
答案 0 :(得分:34)
我可以添加一些关于GHC优化系统的重要细节。
foldr
的幼稚定义绕过一个函数。调用函数有一个固有的开销 - 特别是在编译时不知道该函数时。如果在编译时已知函数的内容,那么能够内联函数的定义真的很棒。
有一些技巧可以在GHC中执行内联 - 这就是它们的一个例子。首先,foldr
需要内联(我以后会说明原因)。 foldr
天真的实现是递归的,所以不能内联。因此,将工作者/包装器转换应用于定义。 worker是递归的,但包装器不是。这允许foldr
内联,尽管递归列表结构。
内联foldr
时,它也会创建所有本地绑定的副本。它或多或少是一个直接的文本内联(模数重命名,并在desugaring传递之后发生)。这是事情变得有趣的地方。 go
是一个本地绑定,优化器可以查看它。它注意到它在本地范围内调用一个函数,它命名为k
。 GHC通常会完全删除k
变量,并将其替换为表达式k
减少到的变量。然后,如果函数应用程序适合内联,则可以在此时内联 - 消除完全调用第一类函数的开销。
让我们看一个简单,具体的例子。此程序将回显一行输入,删除所有尾随'x'
个字符:
dropR :: Char -> String -> String
dropR x r = if x == 'x' && null r then "" else x : r
main :: IO ()
main = do
s <- getLine
putStrLn $ foldr dropR "" s
首先,优化器将内联foldr
的内容并进行简化,从而产生如下代码:
main :: IO ()
main = do
s <- getLine
-- I'm changing the where clause to a let expression for the sake of readability
putStrLn $ let { go [] = ""; go (x:xs) = dropR x (go xs) } in go s
工作包装器转换允许的事情......我将跳过剩下的步骤,但很明显,GHC现在可以内联dropR
的定义,消除函数调用开销。这是获得巨大成功的地方。
答案 1 :(得分:15)
GHC无法内联递归函数,所以
foldr :: (a -> b -> b) -> b -> [a] -> b
foldr _ z [] = z
foldr f z (x:xs) = f x (foldr f z xs)
无法内联。但是
foldr k z = go
where
go [] = z
go (y:ys) = y `k` go ys
不是递归函数。它是一个带有局部递归定义的非递归函数!
这意味着,正如@bheklilr写的那样,map (foldr (+) 0)
foldr
可以内联f
,z
和(+)
替换为0
和{{} 1}}在新的go
中,可能会发生很多事情,例如中间值的拆箱。
答案 2 :(得分:14)
正如评论所说:
-- Inline only in the final stage, after the foldr/cons rule has had a chance
-- Also note that we inline it when it has *two* parameters, which are the
-- ones we are keen about specialising!
特别注意&#34;当它有两个参数时,我们会内联它,这是我们热衷于专业化的参数!&#34;
这说的是,当foldr
被内联时,它仅针对特定选项f
和z
进行内联,而不是列表的选择折叠起来。我不是专家,但它似乎可以在像
map (foldr (+) 0) some_list
以便内联在此行中发生,而不是在应用map
之后。这使得它在更多情况下更容易优化。所有辅助函数都掩盖了第三个参数,因此{-# INLINE #-}
可以做到这一点。
答案 3 :(得分:7)
其他答案中没有提到的一个很小的重要细节是GHC,给出了像
这样的函数定义f x y z w q = ...
在应用所有参数f
,x
,y
,z
和w
之前,无法内联q
。这意味着使用worker / wrapper转换来公开一组最小的函数参数通常是有利的,这些函数参数必须在内联之前应用。