额外的蜥蜴和尾巴的目的是什么.在F#实现与C#?

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)

(两者都处于发布模式).开头有一个额外的小窍门,我并不太好奇,但有趣的是附加ldnulltail.说明.

我的猜测(可能是错误的)是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.