1. 程式人生 > >C# 使用 Emit動態生成函數,附帶與反射,硬編碼的測試結果。

C# 使用 Emit動態生成函數,附帶與反射,硬編碼的測試結果。

命名空間 需要 之前 空間 快的 media dynamic 能夠 高級

  因為托管.Net 程序代碼最終被編譯的結果為CIL(Common Intermediate Language,直譯為公共中間語言,在很多場景下也稱MSIL),在運行時,經過CLR加載執行類型可用性,安全性檢查,並最終由JIT根據本地CPU的指令集生成對應的本地代碼以執行,

所以理論而言,我們可以使用CIL構建最終生成的程序集,當然,前提是使用者必須懂得一些CIL,好在相對於匯編語言,CIL要可讀性要更強,難度更低,BCL(Basic Class Library,基本類庫)提供了Emit方式以供使用者能夠直接構建CIL代碼,相關API對應的命名空間為System.Reflection.Emit;

  本人才疏學淺,現學現賣,本隨筆簡單介紹一下如何使用CIL構建一個簡單的數積函數,一來方便為不懂的人能夠提供入門的理解,二來方便自己記憶,三來希望本隨筆若有不實或不恰當的地方,請各位大佬在評論區或者發送到我的郵箱[email protected]指正.

    /// <summary>
    /// 獲取一個使用Emit創建的簡單的乘法的委托;
    /// </summary>
    /// <returns></returns>
    private static Func<int, int, int> CreateEmitMultiDelegate() {
        
var addMethod = new DynamicMethod(nameof(Multi), typeof(int), new Type[] { typeof(int), typeof(int) }); var ilgenerator = addMethod.GetILGenerator(); ///將第0(1)個參數入棧,此處使用了參數入棧的精簡指令格式<see cref="OpCodes.Ldarg_0"/>, ///對應的參數非精簡指令語句為 ///ilgenerator.Emit(OpCodes.Ldarg, 0); ilgenerator.Emit(OpCodes.Ldarg_0);
//同上,將第1(2)個參數入棧 ilgenerator.Emit(OpCodes.Ldarg_1); ///使用乘法指令以進行數積運算,此操作會將棧中的兩個元素取出,並將數積結果壓入棧頂。 ///由於CIL並非最終生成的代碼,在內存的位運算中究竟如何處理數積運算,將由JIT處理得到; ilgenerator.Emit(OpCodes.Mul); ///返回,對於CIL代碼函數,無論其是否具備返回值,都必須顯示指定返回,否則CLR將會拋出程序不可用異常。 ///對於具備返回值的函數,在返回時,棧中有且只能存在一個元素。 ///反之,棧須為空。 ilgenerator.Emit(OpCodes.Ret); return addMethod.CreateDelegate(typeof(Func<int, int, int>)) as Func<int, int, int>; }

上面的代碼完成了一個非常簡單的,未進行溢出檢查的Int32乘積函數,翻譯為高級語言,也就是我們今天將要使用的代碼,它非常簡單。

 /// <summary>
    /// 乘法函數;
    /// </summary>
    /// <param name="number0"></param>
    /// <param name="number1"></param>
    /// <returns></returns>
    private static int Multi(int number0, int number1) => number0 * number1;

其經過發布模式編譯,逆向得到的代碼如下.(順帶一提,我使用的逆向工具為dnSpy).

// Token: 0x06000002 RID: 2 RVA: 0x00002173 File Offset: 0x00000373
.method private hidebysig static 
    int32 Multi (
        int32 number0,
        int32 number1
    ) cil managed 
{
    // Header Size: 1 byte
    // Code Size: 4 (0x4) bytes
    .maxstack 8

    /* (51,57)-(51,74) E:\CilDemoSolution\ConsoleDemo\Program.cs */
    /* 0x00000374 02           */ IL_0000: ldarg.0
    /* 0x00000375 03           */ IL_0001: ldarg.1
    /* 0x00000376 5A           */ IL_0002: mul
    /* 0x00000377 2A           */ IL_0003: ret
} // end of method Program::Add

可以看到,其主要內容和之前使用Emit的代碼是相呼應的。

最後我們使用反射,Emit,硬編碼三種方式進行1000_000(一千萬次),2048 * 2048的運算。

測試代碼如下,其中EmitTest為測試類,以上的兩端代碼中出現的方法均在本測試類中。

sw.Restart();
            for (int i = 0; i < times; i++) {
                res = Multi(addNum0, addNum1);
            }
            sw.Stop();
            Trace.WriteLine($"Code: {sw.ElapsedMilliseconds}");
        }

使用發布模式編譯多次運行測試後得到以下平均結果:

Debug Trace:
Reflection:3361
Emit: 30
Code: 3

通過結果可以看到反射的方式是比Emit慢幾乎兩個數量級左右,而Emit比硬編碼慢一個數量級左右,代碼中可能存在著諸多不合理之處影響了測試結果的準確性,比如調用Emit函數的方式可能具有更快的方法,反射方式存在進一步優化空間,但三者的性能優先級應該是硬編碼>Emit>反射。

再使用調試模式生成並運行得到的結果如下:

Debug Trace:
Reflection:3387
Emit: 42
Code: 38

在調試模式下,Emit和硬編碼性能相差是不大的,沒有發布模式下那麽大的差距,而這兩種模式的硬編碼方式所生成的CIL代碼是一致的,這是我沒太明白的地方——究竟調試和發布在什麽地方出現了不同的差異導致其在運行時的性能差距如此之大?

最後,我來理解一下使用Emit的場景,在極少數需要靈活的(當然,這也取決於你的使用場景)情況下,我們需要動態地生成代碼以減少重復的勞動以及人工的原因導致的錯誤,根據某些必要信息動態地生成行為。在多數生產環境下,Emit代碼的首次生成仍然需要借助反射,在首次執行反射完畢後,之後的執行將不再需要反射,這種方式可以更好地提升性能,以減少過多地通過反射訪問元數據的方式帶來的拆裝箱,類型轉換等性能影響,這也是很多ORM框架正在使用的方式之一。

C# 使用 Emit動態生成函數,附帶與反射,硬編碼的測試結果。