额外的 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,我对此不太感兴趣,但有趣的是额外的 ldnull
和 tail.
指令。
我的猜测(可能是错误的)是 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
的尾部。
以下 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,我对此不太感兴趣,但有趣的是额外的 ldnull
和 tail.
指令。
我的猜测(可能是错误的)是 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
的尾部。