高阶函数调用的闭包转换和单独编译
Posted
技术标签:
【中文标题】高阶函数调用的闭包转换和单独编译【英文标题】:Closure conversion and separate compilation of higher-order function calls 【发布时间】:2011-01-18 23:59:38 【问题描述】:在编译高阶函数调用时,是否有标准的方法来处理单独编译和不同类型的闭包转换之间的交互?
我知道三种类似函数的构造,它们在大多数编程语言中都有明显的编译:闭包、(***)函数和 C++ 风格的函数对象。从语法上讲,它们的调用方式相同,但编译器会以最佳方式生成形状各异的调用点:
Syntax: | clo(args) | func(args) | obj(args)
--------------------------------------------------------------------------------
Codegen: | clo.fnc(&clo.env, args) | func(args) | cls_call(&obj, args)
^ ^ ^ ^ ^
fn ptr | +--"top level" fn --+ |
+--- "extra" param, compared to source type -----+
(在 C++ 中,cls_call
将是 T::operator()
用于 obj
的类 T
。C++ 也允许使用虚函子,但这本质上是带有额外间接的闭包情况。)
此时,对map (x => x > 3) lst
和map (x => x > y) lst
的调用应该调用不同的map
函数,因为第一个是提升后的简单函数指针,第二个是闭包。
我能想到四种处理这个问题的方法:
C++ (98) 方法,它强制被调用者要么选择调用点形状(通过形式参数类型:虚拟函子、函数指针或非虚拟函子),要么通过使用模板,有效地指定下面的解决方案 #2。
重载:编译器可以对map
和所有其他高阶函数进行多次实例化,并进行适当的名称修改。实际上,每个调用站点形状都有一个单独的内部函数类型,并且重载解决方案选择了正确的类型。
要求一个全球统一的呼叫站点形状。这意味着所有***函数都采用显式的env
参数,即使它们不需要它,并且必须引入“额外的”闭包来包装非闭包参数。
保留***函数的“自然”签名,但要求所有高阶函数参数的处理都必须通过闭包来完成。已关闭函数的“额外”闭包调用包装蹦床函数以丢弃未使用的env
参数。这似乎比选项 3 更优雅,但更难有效实施。编译器要么生成大量调用约定无关的包装器,要么使用少量调用约定敏感的 thunk...
拥有一个优化的闭包转换/lambda提升混合方案,每个函数都可以选择是否将给定的闭包参数粘贴在 env 或参数列表中,这似乎会使问题更加严重。
无论如何,问题:
此问题在文献中有明确的名称吗? 除了以上四种,还有其他方法吗? 方法之间是否存在众所周知的权衡?【问题讨论】:
【参考方案1】:这是一个很深的问题,有很多影响,我不想在这里写一篇学术文章。我将只触及表面,并将向您指出其他地方的更多信息。我的回复基于个人使用 Glorious Glasgow Haskell Compiler 和 Standard ML of New Jersey 的经验,以及有关这些系统的学术论文。
雄心勃勃的编译器的主要区别在于 known 调用和 unknown 调用之间的区别。对于具有高阶函数的语言,次要但仍然很重要的区别是调用是否完全饱和(我们只能在已知调用站点上决定)。 p>
已知调用表示编译器确切知道正在调用什么函数以及它需要多少参数的调用站点。
未知调用意味着编译器无法确定可能调用的函数。
如果被调用的函数正在获取它所期望的所有参数,那么已知调用是完全饱和,并且它会直接进行编码。如果函数获得的参数少于预期,则函数部分应用并且调用只会分配闭包
例如,如果我编写 Haskell 函数
mapints :: (Integer -> a) -> [a]
mapints f = map f [1..]
那么对map
的调用已知并且完全饱和。
如果我写
inclist :: [Integer] -> [Integer]
inclist = map (1+)
那么对map
的调用已知并且部分应用。
最后,如果我写
compose :: (b -> c) -> (a -> c) -> (a -> c)
compose f g x = f (g x)
那么对f
和g
的调用都是未知。
成熟的编译器做的主要事情是优化已知调用。在你上面的分类中,这个策略主要属于#2。
1234563事情进展顺利。如果某个函数的部分调用点(但不是全部)已知,编译器可能会认为值得为 已知 调用创建一个特殊用途的调用约定,该调用将被内联或者将使用只有编译器知道的特殊名称。源代码中以名称导出的函数将使用标准调用约定,其实现通常是对专用版本进行优化尾调用的薄层。
如果已知调用未完全饱和,编译器只会生成代码以在调用者那里分配闭包。
闭包的表示(或者一等函数是否由其他技术处理,例如 lambda 提升或去功能化)在很大程度上与已知调用和未知调用的处理正交。
(值得一提的是MLton 使用的另一种方法:它是一个全程序编译器;它可以查看所有源代码;它使用我忘记的技术将所有函数简化为一阶。还有仍然未知的调用,因为高阶语言中的一般控制流分析是难以处理的。)
关于你最后的问题:
我认为这个问题只是“如何编译一流函数”这一混乱问题的一个方面。我从来没有听说过这个问题的特殊名称。
是的,还有其他方法。我画了一个草图并提到了另一个。
我不确定是否有任何关于权衡的伟大而广泛的研究,但我所知道的最好的,我强烈推荐的,是 Simon Marlow 和 Simon Peyton Jones 的Making a Fast Curry: Push/Enter vs. Eval/Apply for Higher-Order Languages。这篇论文的一大优点是它解释了为什么函数的类型不告诉你对该函数的调用是否完全饱和。
总结您的编号替代方案:编号 1 是行不通的。 流行的编译器使用与数字 2 和 3 相关的混合策略。 我从未听说过类似数字 4 的东西。区分已知调用和未知调用似乎比区分***函数和函数类型的参数更有用。
【讨论】:
以上是关于高阶函数调用的闭包转换和单独编译的主要内容,如果未能解决你的问题,请参考以下文章