Asi*_*sik 12 c# f# cil tail-recursion tail-call-optimization
以下C#函数:
T ResultOfFunc<T>(Func<T> f)
{
return f();
}
Run Code Online (Sandbox Code Playgroud)
毫不奇怪地汇编到这个:
IL_0000: ldarg.1
IL_0001: callvirt 05 00 00 0A
IL_0006: ret
Run Code Online (Sandbox Code Playgroud)
但是等效的F#功能:
let resultOfFunc func = func()
Run Code Online (Sandbox Code Playgroud)
汇编到这个:
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldnull
IL_0003: tail.
IL_0005: callvirt 04 00 00 0A
IL_000A: ret
Run Code Online (Sandbox Code Playgroud)
(两者都处于发布模式).开头有一个额外的小窍门,我并不太好奇,但有趣的是附加ldnull和tail.说明.
我的猜测(可能是错误的)是ldnull必要的,如果函数是void这样,它仍然返回something(unit),但这并不能解释tail.指令的目的是什么.如果函数确实在栈上推送了某些东西会发生什么呢?是不是它会被一个不会弹出的额外null所困?
Fyo*_*kin 17
C#和F#版本有一个重要的区别:C#函数没有任何参数,但F#版本有一个类型的参数unit.该unit值显示为ldnull(因为null它被用作唯一unit值的表示()).
如果您要将第二个函数转换为C#,它将如下所示:
T ResultOfFunc<T>( Func<Unit, T> f ) {
return f( null );
}
Run Code Online (Sandbox Code Playgroud)
至于.tail指令 - 即所谓的"尾调用优化".
在常规函数调用期间,返回地址被压入堆栈(CPU堆栈),然后调用该函数.当函数完成时,它执行"返回"指令,该指令将返回地址弹出堆栈并在那里传输控制.
但是,当函数A调用函数B,然后立即返回函数B的返回值,而不做任何其他操作时,CPU可以跳过推送堆栈上的额外返回地址,并执行" 跳转 " B而不是" 调用 ".这样,当B执行"返回"指令时,CPU将从堆栈中弹出返回地址,并且该地址将指向不是A,而是指向A首先调用的任何人.
另一种思考方式是:函数A调用函数B不是在返回之前,而是返回,而不是返回,因此委托返回的荣誉B.
所以实际上,这种神奇的技术允许我们在不消耗堆栈上的位置的情况下进行调用,这意味着您可以执行任意多次此类调用而不会有堆栈溢出的风险.这在函数式编程中非常重要,因为它允许有效地实现递归算法.
它被称为"尾调用",因为调用B发生时,这么说来,在尾部的A.