额外的ldnull和tail的目的是什么?f#实现vs c#

本文关键字:实现 vs 是什么 ldnull tail | 更新日期: 2023-09-27 17:51:07

以下c#函数:

T ResultOfFunc<T>(Func<T> f)
{
    return f();
}

的编译结果如下:

IL_0000:  ldarg.1     
IL_0001:  callvirt    05 00 00 0A 
IL_0006:  ret  
而等价的f#函数:
let resultOfFunc func = func()

编译成:

IL_0000:  nop         
IL_0001:  ldarg.0     
IL_0002:  ldnull      
IL_0003:  tail.       
IL_0005:  callvirt    04 00 00 0A 
IL_000A:  ret 

(两者都处于释放模式)。开头有一个额外的nop,我不是很好奇,但有趣的是额外的ldnulltail.指令。

我的猜测(可能是错误的)是,ldnull是必要的,如果函数是void,所以它仍然返回的东西(unit),但这并不能解释什么是tail.指令的目的。如果函数确实在栈上压入了什么会发生什么,它不是被一个额外的空卡住了吗?

额外的ldnull和tail的目的是什么?f#实现vs c#

c#和f#版本有一个重要的区别:c#函数没有任何参数,但f#版本有一个类型为unit的参数。unit的值就是显示为ldnull的值(因为null被用作唯一的unit()的表示)。

如果你把第二个函数翻译成c#,它看起来像这样:

T ResultOfFunc<T>( Func<Unit, T> f ) {
   return f( null );
}

对于.tail指令——即所谓的"尾部调用优化"。
在常规函数调用期间,返回地址被压入堆栈(CPU堆栈),然后调用函数。当函数完成后,它执行"return"指令,该指令将返回地址从堆栈中弹出并将控制转移到那里。
然而,当函数A调用函数B,然后立即返回函数B的返回值,而不做任何其他事情,CPU可以跳过在堆栈上推送额外的返回地址,并执行"跳转"到B,而不是"调用"。这样,当B执行"return"指令时,CPU将从堆栈中弹出返回地址,该地址不会指向A,而是指向最初调用A的人。
另一种思考方式是:函数A调用函数B时,在返回之前不是,而是而不是返回,这样就把返回的荣誉委托给了B

因此,实际上,这种神奇的技术允许我们在不消耗堆栈上的一个位置的情况下调用,这意味着您可以执行任意多个这样的调用而不会冒堆栈溢出的风险。这在函数式编程中非常重要,因为它允许有效地实现递归算法。

它被称为"尾部调用",因为对B的调用发生在A的尾部