Optimization 函数式编程中的高效递归与不同范式中的低效递归
据我所知,递归非常优雅,但在OOP和过程编程中效率不高(请参阅精彩的“高阶perl”,Mark Jason Dominus)。我有一些信息,在函数编程中递归是快速的——保持它的优雅和简单。有人能证实并可能放大这一点吗?我在考虑XSLT和Haskell(在我的下一个要学习的语言列表中排名靠前) 谢谢Optimization 函数式编程中的高效递归与不同范式中的低效递归,optimization,xslt,haskell,functional-programming,tail-recursion,Optimization,Xslt,Haskell,Functional Programming,Tail Recursion,据我所知,递归非常优雅,但在OOP和过程编程中效率不高(请参阅精彩的“高阶perl”,Mark Jason Dominus)。我有一些信息,在函数编程中递归是快速的——保持它的优雅和简单。有人能证实并可能放大这一点吗?我在考虑XSLT和Haskell(在我的下一个要学习的语言列表中排名靠前) 谢谢 DanielOOP/过程语言倾向于在每次进行递归调用时将数据放在堆栈上-因此,在这些语言中,递归不如迭代有效 相比之下,函数式语言的编译器/解释器通常设计为优化尾部递归,如下所示: 递归可能需要维护堆
DanielOOP/过程语言倾向于在每次进行递归调用时将数据放在堆栈上-因此,在这些语言中,递归不如迭代有效 相比之下,函数式语言的编译器/解释器通常设计为优化尾部递归,如下所示: 递归可能需要维护堆栈,但编译器可以识别尾部递归并将其优化为用于在命令式语言中实现迭代的相同代码。Scheme编程语言标准要求实现识别和优化尾部递归。尾部递归优化可以通过在编译期间将程序转换为延续传递样式以及其他方法来实现
并且有更详细的信息。如果编译器没有对语言进行优化,递归很可能比迭代慢,因为在沿着给定的路径下降(这相当于迭代)的基础上,您必须在完成任务后回溯您的步骤
否则,它几乎是等价的,只是它可能更优雅,因为您让编译器和系统在幕后处理循环。当然,也有一些任务(比如处理树状结构)递归是唯一的方法(或者至少是唯一一种不会令人绝望地卷积的方法)。如果使用的编译器支持尾部调用优化,并且您构造代码来利用它,那么递归并不是低效的
由于递归在函数式编程中的普遍存在,函数式语言的编译器更可能实现过程式语言的尾部调用优化。函数式语言中递归的快速之处在于编译器可以使用尾部递归消除在内部将递归转换为迭代(或更一般地说,尾部呼叫消除)。基本上,如果递归调用是函数返回之前的最后一个操作,并且函数的返回值是递归调用的返回值,则程序将重用当前帧,而不是创建新的堆栈帧。参数变量设置为新值,PC设置为函数的开头 利用尾部递归消除需要程序员有所意识。您需要确保递归调用实际上是尾部调用。例如,以下是OCaml中计算阶乘的代码:
let rec factorial n =
if n = 0 then
1
else
n * factorial (n - 1)
尾部调用消除在这里不会直接起作用,因为在递归调用之后必须执行乘法。但是,如果函数被重写为:
let factorial n =
let rec fac_helper n p =
if n = 0 then
p
else
fac_helper (n - 1) (p * n)
in
fac_helper n 1
现在可以使用尾部调用消除。这将转换为如下内容(在伪代码中):
这种风格似乎违反直觉,但它与迭代版本一样有意义。计算的每一步都是在调用递归函数的过程中执行的。在整个计算过程中使用的归纳变量(如上面的p
和n
)被声明为参数
应该注意的是,大多数命令式语言和函数式语言的编译器都利用了这种优化。事实上,LLVM版本的优化甚至允许递归调用和返回之间的关联操作,因此您可以编写阶乘的第一个版本,并且仍然使用优化。但是,尾部调用JVM不支持模拟,因此JVM上的函数语言(如Scala)对模拟的支持有限。尾部递归是任何体面函数语言实现中的迭代。下面是一个使用GHC Haskell的示例。一个添加数字序列的简单程序。它从几个递归函数的组合开始:
import qualified Data.Vector as U
main = print (U.sum (U.enumFromTo 1 (10000000 :: Int)))
编译器将其优化为单尾递归函数(在源到源转换中):
或者使用GHC LLVM后端,对程序的命令式表示应用其他优化:
loop:
leaq 1(%rsi), %rax
addq %rsi, %r14
cmpq $10000001, %rax
jge .LBB1_5
addq $2, %rsi
addq %rax, %r14
test: # %tailrecurse
cmpq $10000001, %rsi
jl loop
注意尾部递归标签是如何标记的
所以我们有一个递归函数管道,它被编译成一个单尾递归函数,它被编译成一个命令循环,不使用堆栈。最后是8条指令
这就是为什么函数组合和递归在良好的优化函数语言中都非常有效。XSLT中的高效递归
在XSLT中实现高效递归的主要方法有两种:
<xsl:function name="my:sum">
<xsl:param name="pAccum" as="xs:double*"/>
<xsl:param name="pNums" as="xs:double*"/>
<xsl:sequence select=
"if(empty($pNums))
then $pAccum
else
my:sum($pAccum + $pNums[1], $pNums[position() >1])
"
/>
</xsl:function>
<xsl:function name="my:sum2">
<xsl:param name="pNums" as="xs:double*"/>
<xsl:sequence select=
"if(empty($pNums))
then 0
else
if(count($pNums) eq 1)
then $pNums[1]
else
for $half in count($pNums) idiv 2
return
my:sum2($pNums[not(position() gt $half)])
+
my:sum2($pNums[position() gt $half])
"
/>
</xsl:function>
DVC的主要思想是将输入序列细分为两个(通常)或多个部分,并相互独立地进行处理,然后组合结果以生成整个输入序列的结果
请注意,对于N
项序列,在od时间的任何时间点调用堆栈的最大深度都不会超过log2(N)
,,这对于大多数实际用途来说已经足够了。例如,处理1000000(1M)项序列时,调用堆栈的最大深度仅为19
虽然有些XSLT处理器不够智能,无法识别和优化尾部递归,但DVC递归模板可以在任何XSLT处理器上工作。不要假设递归与迭代是对立的
loop:
leaq 1(%rsi), %rax
addq %rsi, %r14
cmpq $10000001, %rax
jge .LBB1_5
addq $2, %rsi
addq %rax, %r14
test: # %tailrecurse
cmpq $10000001, %rsi
jl loop
<xsl:function name="my:sum">
<xsl:param name="pAccum" as="xs:double*"/>
<xsl:param name="pNums" as="xs:double*"/>
<xsl:sequence select=
"if(empty($pNums))
then $pAccum
else
my:sum($pAccum + $pNums[1], $pNums[position() >1])
"
/>
</xsl:function>
<xsl:function name="my:sum2">
<xsl:param name="pNums" as="xs:double*"/>
<xsl:sequence select=
"if(empty($pNums))
then 0
else
if(count($pNums) eq 1)
then $pNums[1]
else
for $half in count($pNums) idiv 2
return
my:sum2($pNums[not(position() gt $half)])
+
my:sum2($pNums[position() gt $half])
"
/>
</xsl:function>