早前大家都是用 “委托” 的方式来调用,这会增加一层额外不必要的存根开销,而在 C# 9.0 语法之中编译器提供了新的特性支持,可以显著提高调用函数的性能。
Microsoft 官方详细开发者文档:不安全代码、数据指针和函数指针 | Microsoft Docs
Function pointers - C# 9.0 specification proposals | Microsoft Docs
OpCodes.Calli 字段 (System.Reflection.Emit) | Microsoft Docs
.NET运行时内缺省的 “委托”,最大的问题是会编译很多的 JIT Stub 函数副本, 被委托的函数每次被调用时都需要先执行这些存根跳板函数,基于微软官方的原生编译器编译后的 C# 委托调用性能就很高,但运行在托管环境下委托性能要差上不少。
另外,大家都知道 .NET 托管函数的地址,可以通过反射机制来获取(获取时则JIT会即时编译,原生编译后无法通过反射获取函数地址),而且要通过函数地址来调用 .NET 托管函数必须使用委托,现在这些编程上面的疑难问题已被解决。
适用 C# 9.0 需要配置 Visual C# 解决方案工程项目文件,在 “
- public static long Add(int x, int y) => x + y;
-
- [MTAThread]
- private static void Main(string[] args)
- {
- unsafe
- {
- delegate* managed<int, int, long> pfAdd = &Add;
- pfAdd(0, 0);
- }
- }
- delegate*<int, int, long> pfAdd = &Add;
- 等价
- delegate* managed<int, int, long> pfAdd = &Add;
MSIL中间语言
- ldftn int64 Module.Program::Add(int32, int32)
- stloc.0
- ldloc.0
- stloc.1
- ldc.i4.0
- ldc.i4.0
- ldloc.1
- calli int64(int32, int32)
- pop
- ret
以前需要适用 C/C++ .NET(C/C++ CLI)才可以编写类似的调用,而 C# 上面虽然可以 Emit 内联MSIL来实现,但这并不是人们想要的,因为它会额外增加了大量函数调用开销。
C# 语言编译的中间代码,曾经只会用到两个 Call 操作数。
1、Call
用于静态函数
2、Callvirt
用于实例函数,缺点需要查找虚表,额外增加开销
一个有意思的话题,struct 实例函数是上述哪一类,还是两类都有?答案是两者都有,class 实例函数也是相同的。
只不过 struct 实例函数均为 Call,指非框架运行时系统特殊的函数,例如:GetHashCode()、ToString()。
函数指针,编程限制:
managed 函数指针可以被强制转换为 unmanaged,但存在函数调用安全问题,因为托管函数的调用协议为 __fastcall,但 C# 函数指针的特性上为 __cdcel C函数调用协议,强制转换函数的指针,不能确保强制转换以后的调用协议与 __cdcel 保持一致性,不建议的危险编码行为。
- unsafe
- {
- delegate* managed<int, int, long> proc = &Add;
- delegate* unmanaged[Cdecl]<int, int, long> add = (delegate* unmanaged[Cdecl]<int, int, long>)proc;
- Console.WriteLine(add(1, 2));
- }
注意:AMD x86_64 上面为 __cdcel + System-V AMD64-ABI 函数调用协议。System V AMD64-ABI Calling Convention by GCC_liulilittle的博客-CSDN博客
函数指针不可以被 box 操作数装箱对象,必须在 unsafe 不安全代码上下文内适用。
函数指针多个转换例子及AMD x86_64环境下,可以采用 __stdcall(WINAPI)调用函数,但这是有限度的,在PUSH参数使用CPU寄存器的情况下不会改变堆栈RSP,那么使用 __cdcel、__stdcall 是没有什么区别的,但如果PUSH参数数量太多,那么就涉及到两者调用协议平衡堆栈的方式,__cdcel 由去调用者平衡堆栈、__stdcall 由被调用方平衡堆栈,这涉及到当前函数栈能否回到上个栈帧CALL EIP位置。
- unsafe
- {
- delegate* managed<int, int, long> proc = &Add;
- IntPtr sysPtr = (IntPtr)proc;
- void* navPtr = proc;
-
- delegate* unmanaged[Stdcall]<int, int, long> add0 = (delegate* unmanaged[Stdcall]<int, int, long>)proc;
- delegate* unmanaged[Stdcall]<int, int, long> add1 = (delegate* unmanaged[Stdcall]<int, int, long>)sysPtr;
- delegate* unmanaged[Stdcall]<int, int, long> add2 = (delegate* unmanaged[Stdcall]<int, int, long>)navPtr;
- Console.WriteLine(add0(1, 2));
- Console.WriteLine(add1(1, 2));
- Console.WriteLine(add2(1, 2));
- }