[料理佳餚] 用 C# 的 System.Reflection.Emit 撰寫 IL Code 將值指派給私有欄位(Private Field)

程式寫多了,我們多多少少會開發一些 Library 來輔助我們讓程式寫起來更方便一些,這些 Library 通常都獨立於專案之外,除了使用上的彈性之外,還有一個我們會關注的大概就是效能了,既然提到了效能,我們腦海中閃過的解決方案應該會有「撰寫 IL Code」這個選項,IL Code 雖然可讀性極差,但是如果我們有能力可以讀得懂,甚至使用 IL Code 撰寫程式的話,對我們在程式執行狀況的掌握,絕對有正向的提昇。

我就用 IL Code 撰寫一段將值指派給私有欄位的程式來當個起頭,之後如果還有遇到 IL Code 可以發揮的地方,我也會儘量整理成文章分享給大家。

首先,不知道什麼是 IL 的朋友,就參考底下這幾篇文章自行科普一下。

起手式

一般我們要撰寫 IL Code 最快速的方式,就是建立一個 DynamicMethod 物件,呼叫 GetILGenerator() 方法取得 ILGenerator 物件,接著就可以開始撰寫 IL Code。

var dynamicMethod = new DynamicMethod("Method Name", typeof(ReturnType), new[] { typeof(ParameterType1), typeof(ParameterType2) }, true);

var ilGenerator = dynamicMethod.GetILGenerator();

// ...

除了 DynamicMethod 之外,還有一個取得 ILGenerator 的方式,是從 MethodBuilder 來,這個就比較累人,因為得一路從 AssemblyBuilderModuleBuilderTypeBuilder 建起來,等於是在執行時期憑空捏造一個組件,這事我也幹過,之後也會整理成文章跟大家分享。

撰寫指派私有欄位的 IL Code

我的實驗環境是這樣的,我有一個 PrivateFieldSetter 的類別,底下有一個私有欄位 myData,還有三個方法 SetMyDataDirectly()SetMyDataByILCode()SetMyDataByReflection(),對應著三種不同指派值的方式,待會兒也會對這三種方法做 Benchmark。

public class PrivateFieldSetter
{
    private static readonly FieldInfo MyDataField = typeof(PrivateFieldSetter).GetField("myData", BindingFlags.NonPublic | BindingFlags.Instance);
    private static readonly Action<MyData, object> MyDataSetter = BuildMyDataSetter();
    private MyData myData;

    public void SetMyDataDirectly()
    {
        this.myData = new MyData();
    }

    public void SetMyDataByILCode()
    {
        MyDataSetter(new MyData(), this);
    }

    public void SetMyDataByReflection()
    {
        MyDataField.SetValue(this, new MyData());
    }

    private static Action<MyData, object> BuildMyDataSetter()
    {
        var setterMethod = new DynamicMethod("Set_myData_By_ILCode", null, new[] { typeof(MyData), typeof(object) }, true);
        var ilGenerator = setterMethod.GetILGenerator();

        ilGenerator.Emit(OpCodes.Ldarg_1);
        ilGenerator.Emit(OpCodes.Ldarg_0);
        ilGenerator.Emit(OpCodes.Stfld, MyDataField);
        ilGenerator.Emit(OpCodes.Ret);

        return setterMethod.CreateDelegate(typeof(Action<MyData, object>)) as Action<MyData, object>;
    }
}

其中 SetMyDataByILCode() 方法就是我們今天的主角了,我們專注下面這四行程式碼就好,說明就寫在註解。

ilGenerator.Emit(OpCodes.Ldarg_1);              // 載入第 2 個參數到 Evaluation Stack
ilGenerator.Emit(OpCodes.Ldarg_0);              // 載入第 1 個參數到 Evaluation Stack
ilGenerator.Emit(OpCodes.Stfld, MyDataField);   // 指派第 1 個參數給第 2 個參數的 MyDataField
ilGenerator.Emit(OpCodes.Ret);                  // 回傳

Benchmark

最後我們用 BenchmarkDotNet 來測試 SetMyDataDirectly()、SetMyDataByILCode()、SetMyDataByReflection() 這三個方法的執行速度,跟預期的一樣,一般的 Reflection 是最慢的,其次是 IL Code,最快是靜態的直接指派。

參考資料

C# 指南 ASP.NET 教學 ASP.NET MVC 指引
Azure SQL Database 教學 SQL Server 教學 Xamarin.Forms 教學