程式寫多了,我們多多少少會開發一些 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,最快是靜態的直接指派。