额外的 ldnull 和 tail 的目的是什么。在 F# 实现与 C# 中?

What is the purpose of the extra ldnull and tail. in F# implementation vs C#?

以下 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 所以它仍然是 returns 东西(unit),但事实并非如此' 解释 tail. 指令的目的是什么。如果该函数确实将某些内容压入堆栈,会发生什么情况,它是否会卡住一个不会弹出的额外空值?

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

如果将第二个函数转换为 C#,它将如下所示:

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

至于.tail指令——也就是所谓的"tail call optimization"。
在常规函数调用期间,return 地址被压入堆栈(CPU 堆栈),然后调用该函数。函数完成后,它会执行 "return" 指令,将 return 地址弹出堆栈并将控制转移到那里。
但是,当函数 A 调用函数 B 时,然后立即 return 函数 B 的 return 值,没有做任何其他事情, CPU 可以跳过将额外的 return 地址压入堆栈,并执行“jump” 到 B 而不是“call”。这样,当 B 执行 "return" 指令时, CPU 将从堆栈中弹出 return 地址,并且该地址不会指向 A,但是给第一个打电话给 A 的人。
另一种思考方式是:函数 A 调用函数 B 而不是 returning 之前,而是 而不是 returning,因此将 returning 的荣誉授予 B.

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

它被称为"tail call",因为调用了B,也就是说,A的尾部