• C# 9.0语法标准 “函数指针” 深度解读!


    早前大家都是用 “委托” 的方式来调用,这会增加一层额外不必要的存根开销,而在 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# 解决方案工程项目文件,在 “” XML节点之中增加 “9.0” 或 “10.0”,保存重新载入工程解决方案。

    1. public static long Add(int x, int y) => x + y;
    2. [MTAThread]
    3. private static void Main(string[] args)
    4. {
    5. unsafe
    6. {
    7. delegate* managed<int, int, long> pfAdd = &Add;
    8. pfAdd(0, 0);
    9. }
    10. }
    1. delegate*<int, int, long> pfAdd = &Add;
    2. 等价
    3. delegate* managed<int, int, long> pfAdd = &Add;

     MSIL中间语言

    1. ldftn int64 Module.Program::Add(int32, int32)
    2. stloc.0
    3. ldloc.0
    4. stloc.1
    5. ldc.i4.0
    6. ldc.i4.0
    7. ldloc.1
    8. calli int64(int32, int32)
    9. pop
    10. 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 保持一致性,不建议的危险编码行为。

    1. unsafe
    2. {
    3. delegate* managed<int, int, long> proc = &Add;
    4. delegate* unmanaged[Cdecl]<int, int, long> add = (delegate* unmanaged[Cdecl]<int, int, long>)proc;
    5. Console.WriteLine(add(1, 2));
    6. }

    注意: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位置。

    1. unsafe
    2. {
    3. delegate* managed<int, int, long> proc = &Add;
    4. IntPtr sysPtr = (IntPtr)proc;
    5. void* navPtr = proc;
    6. delegate* unmanaged[Stdcall]<int, int, long> add0 = (delegate* unmanaged[Stdcall]<int, int, long>)proc;
    7. delegate* unmanaged[Stdcall]<int, int, long> add1 = (delegate* unmanaged[Stdcall]<int, int, long>)sysPtr;
    8. delegate* unmanaged[Stdcall]<int, int, long> add2 = (delegate* unmanaged[Stdcall]<int, int, long>)navPtr;
    9. Console.WriteLine(add0(1, 2));
    10. Console.WriteLine(add1(1, 2));
    11. Console.WriteLine(add2(1, 2));
    12. }
  • 相关阅读:
    zabbix配置钉钉报警
    为什么电商创业者2022年都在做抖音小店无货源?揭开抖店无货源玩法
    【WALT】scale_exec_time() 代码详解
    聊一聊 HBase 是如何写入数据的?
    JavaScript---DOM---DOM简介、获取元素、事件基础、操作元素---11.5
    DirectXShaderCompiler mac编译
    阿里云短信服务开通
    【Pytorch学习笔记】9.分类器的分类结果如何评估——使用混淆矩阵、F1-score、ROC曲线、PR曲线等(以Softmax二分类为例)
    设计模式-抽象工厂模式
    solr快速上手:managed-schema标签详解(三)
  • 原文地址:https://blog.csdn.net/liulilittle/article/details/126670649