【发布时间】:2018-09-11 09:54:42
【问题描述】:
Calli 操作码需要调用约定。默认是stdcall,而原生库中的extern "C"使用cdecl。
JIT recently allowed 与 calli 内联方法,但仅限于 默认调用约定。当我使用calli 而不使用unmanaged cdecl 调用方法时,它适用于x64,性能比DllImport 快58%,比unmanaged function pointer 快2.2 倍。 (在netcoreapp2.1 上,net471 上的差异更大:82% 和 5.5x )当我使用calli unmanaged cdecl 运行方法时,性能与DllImport 相当(大约慢1%)。
我 have read 在 x64 上不再有 stdcall 与 cdecl 的混乱,所有方法都使用 cdecl(或 fastcall,在另一个地方看到,找不到链接)。差异仅适用于x86,我在没有unmanaged cdecl 的情况下调用确实会使应用程序因段错误而崩溃。
有问题的方法是the following。对于测试,我只使用 noop 原生方法来测量原生调用开销。
.method public hidebysig static int32 CalliCompress(uint8* source, native int sourceLength, uint8* destination, native int destinationLength, int32 clevel, native int functionPtr) cil managed aggressiveinlining
{
.custom instance void System.Runtime.Versioning.NonVersionableAttribute::.ctor()
= {}
//
.maxstack 6
ldarg.0
ldarg.1
ldarg.2
ldarg.3
ldarg 4
ldarg 5
calli unmanaged cdecl int32 (uint8* source, native int sourceLength, uint8* destination, native int destinationLength, int32 clevel)
ret
}
我的问题:
1) 在x64 上“按设计”在calli 之后省略unmanaged cdecl 是否安全,或者我对这个例子很幸运?如果在 x64 上所有调用都是 cdecl,那么我可以使用 JIT treating static readonly fields as constants 分发到适当的方法,只需使用 if(IntPtr.Size == 8) {..call fast method..}else{..use unmanaged cdecl..}
2) caller or callee cleans the stack 是什么意思?我的本机函数返回调用后堆栈上的int。这是关于谁从堆栈中删除此int 的问题吗?或者还有一些其他的工作需要在本机函数中使用堆栈完成?我可以控制本机函数,并且可以通过 ref 参数返回值 - 这是否会使堆栈清理问题变得无关紧要,因为在调用期间没有进行堆栈更改?
【问题讨论】:
-
实际上,运行时仅支持 x64 上的一种调用约定,即“the”调用约定。
cdecl、stdcall、fastcall和thiscall都是一样的。有关详细信息,请参阅here。返回值在 RAX 寄存器中传递,它们不经过堆栈(除非参数超过 64 位,但这与 CLR 调用无关),但无论如何您不必担心清理堆栈,因为 CLR 会发出适当的代码。 -
@JeroenMostert 等 x64 我总是可以使用可内联的
calli而没有unmanaged和任何调用约定? Linux x64 也是如此? -
Linux 上的 x64 调用约定与 Windows 不同(使用不同的寄存器),但确实存在“一个”调用约定,所以我希望答案是“是的”。但我没有在 Linux 上使用非托管互操作的经验。
-
@JeroenMostert 它在 Docker 中工作,默认
calli比 DllImport 快 5 倍(在 VS 的 Release+F5 模式下)。将不得不在 coreclr repo 中询问这是否可以保证工作而不是“发生工作” -
calli可用于调用托管和非托管函数。省略unmanaged会导致它将函数视为托管函数,如果 JITter 碰巧以相同方式调用具有相同签名的托管函数,这可能起作用,但如果它使用其他调用可能会失败约定(这是一个实现细节)。
标签: c# .net cil calling-convention