程式寫多了,我們多多少少會開發一些 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 來,這個就比較累人,因為得一路從 AssemblyBuilder、ModuleBuilder、TypeBuilder 建起來,等於是在執行時期憑空捏造一個組件,這事我也幹過,之後也會整理成文章跟大家分享。
撰寫指派私有欄位的 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,最快是靜態的直接指派。

