方法的三種調用形式
在《可以調用Null的實例方法嗎?》一文中,我談到.NET方法的三種調用形式,現在我們就來著重聊聊這個話題。具體來說,這里所謂的三種方法調用形式對應著三種IL指令:Call、CallVirt和Calli。
一、三個方法調用指令
二、三種方法調用形式
三、虛方法的分發(fā)(virtual dispatch)
四、性能差異
一、三個方法調用指令
雖然C#的方法具有靜態(tài)方法和實例方法之分,但是在IL層面,它們之間并沒有什么不同,就是單純的“函數”而已,而且這個函數的第一個參數的類型永遠是方法所在的類型。所以在IL層面,方法總是“靜態(tài)”的,調用實例方法的本質就是將目標實例作為第一個參數,對于靜態(tài)方法,第一個參數永遠是Null/Default(值類型)。我在《實例方法和靜態(tài)方法有區(qū)別嗎?》中曾經著重談到過這個問題。
Call和CallVirt指令執(zhí)行方法的流程只有兩步:將所有參數壓入棧中 + 執(zhí)行方法。它們之間的不同之處在于:Call指令編譯時就已經確定了執(zhí)行的方法,而CallVirt則是在運行時根據作為第一個參數的實例類型決定最終執(zhí)行的方法。Calli指令則有所不同,我們執(zhí)行該指令時需要指定目標方法的指針,整個流程包括三步:將所有參數壓入棧中 + 將目標方法指針壓入棧中+執(zhí)行方法。
二、三種方法調用形式
接下來我們使用動態(tài)方法的形式演示上述三種方法調用指令的使用。具體來說,我們采用三種方式調用定義在Calculator中用來進行加法運算的Add方法,為此我們利用CreateInvoker方法根據指定的指令生成一個對應的Func<Calculator, int, int, int>委托。在CreateInvoker方法中,我們創(chuàng)建一個與Func<Calculator, int, int, int>委托匹配的動態(tài)方法。在IL Emit過程中,我們先將三個參數(Calculator對象和Add方法的參數a和b)壓入棧中。如果指定的是Call和CallVirt指令,我們直接執(zhí)行它們就可以了。如果指定的是Calli指令,我們得執(zhí)行Ldftn指令將Add方法的指針壓入棧中(方法指針通過指定的MethodInfo對象提供),然后再執(zhí)行Calli指令。
var calculator = new Calculator(); var invoker = CreateInvoker(OpCodes.Call); Console.WriteLine($"1 + 2 = {invoker(calculator, 1, 2)} [Call]"); invoker = CreateInvoker(OpCodes.Callvirt); Console.WriteLine($"1 + 2 = {invoker(calculator, 1, 2)} [Callvirt]"); invoker = CreateInvoker(OpCodes.Calli); Console.WriteLine($"1 + 2 = {invoker(calculator, 1, 2)} [Calli]"); static Func<Calculator, int, int, int> CreateInvoker(OpCode opcode) { var method = typeof(Calculator).GetMethod("Add")!; var dynamicMethod = new DynamicMethod("Add", typeof(int), [typeof(Calculator), typeof(int), typeof(int)]); var il = dynamicMethod.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Ldarg_2); if (opcode == OpCodes.Call) { il.Emit(OpCodes.Call, method); } else if (opcode == OpCodes.Callvirt) { il.Emit(OpCodes.Callvirt, method); } else if (opcode == OpCodes.Calli) { il.Emit(OpCodes.Ldftn, method); il.EmitCalli(OpCodes.Calli, CallingConvention.ThisCall, typeof(int), [typeof(Calculator), typeof(int), typeof(int)]); } il.Emit(OpCodes.Ret); return (Func<Calculator, int, int, int>)dynamicMethod.CreateDelegate(typeof(Func<Calculator, int, int, int>)); } public class Calculator { public virtual int Add(int a, int b) => a + b; }
演示程序利用指定的三種方法指令創(chuàng)建了對應的Func<Calculator, int, int, int>,然后指定相同的參數(Calculator實例、整數1、2)執(zhí)行它們,我們最終會在控制臺上得到如下的輸出結果。
三、虛方法的分發(fā)(virtual dispatch)
雖然Calculator的Add是個虛方法,由于Call指令執(zhí)行的目標方法在編譯時就確定,Calli則是我們以指針的形式指定了執(zhí)行的方法,不論我們指定的目標對象具體是何類型,執(zhí)行的永遠是定義在Calculator類型的那個Add方法。面向對象“多態(tài)”的能力只能通過CallVirt指令來實現。
var calculator = new FakeCalculator(); var invoker = CreateInvoker(OpCodes.Call); Console.WriteLine($"1 + 2 = {invoker(calculator, 1, 2)} [Call]"); invoker = CreateInvoker(OpCodes.Callvirt); Console.WriteLine($"1 + 2 = {invoker(calculator, 1, 2)} [Callvirt]"); invoker = CreateInvoker(OpCodes.Calli); Console.WriteLine($"1 + 2 = {invoker(calculator, 1, 2)} [Calli]"); public class FakeCalculator : Calculator { public override int Add(int a, int b) => a - b; }
以如上的程序為例,我們定義了Calculator的派生類FakeCalculator,在重寫的Add方法中執(zhí)行“減法運算”。我們將這個FakeCalculator對象作為參數調用三個委托,會得到如下所示的輸出結果,可以看出CallVirt指令才能得到我們希望的結果。
當我們在使用Calli指令時, 由于我們是利用Ldfnt指令指定的定義在Calculator類型中的Add方法,所以起不到“多態(tài)”的作用。如果要實現多態(tài),我們得按照如下得方式使用Ldvirtfnt,該指令會從提取目標對象(通過執(zhí)行Ldarg_0指令壓入棧中)并解析出對應得方法。
static Func<Calculator, int, int, int> CreateInvoker(OpCode opcode) { var method = typeof(Calculator).GetMethod("Add")!; var dynamicMethod = new DynamicMethod("Add", typeof(int), [typeof(Calculator), typeof(int), typeof(int)]); var il = dynamicMethod.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Ldarg_2); if (opcode == OpCodes.Call) { il.Emit(OpCodes.Call, method); } else if (opcode == OpCodes.Callvirt) { il.Emit(OpCodes.Callvirt, method); } else if (opcode == OpCodes.Calli) { il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldvirtftn, method); il.EmitCalli(OpCodes.Calli, CallingConvention.ThisCall, typeof(int), [typeof(Calculator), typeof(int), typeof(int)]); } il.Emit(OpCodes.Ret); return (Func<Calculator, int, int, int>)dynamicMethod.CreateDelegate(typeof(Func<Calculator, int, int, int>)); }
再次執(zhí)行演示程序,就會得到我們希望得結果:
四、性能差異
既然Call、CallVirt和Calli都是能幫助我們完成方法的執(zhí)行,我們自然會進一步關系它們的性能差異了,為此我們來做一個簡單的性能測試。
BenchmarkRunner.Run<Test>(); public class Test { private static readonly Func<Calculator, int, int, int> _call = CreateInvoker(OpCodes.Call); private static readonly Func<Calculator, int, int, int> _callvirt = CreateInvoker(OpCodes.Callvirt); private static readonly Func<Calculator, int, int, int> _calli = CreateInvoker(OpCodes.Calli); private static readonly Calculator _calculator = new FakeCalculator(); [Benchmark] public int Call() => _call(_calculator, 1, 2); [Benchmark] public int Callvirt() => _callvirt(_calculator, 1, 2); [Benchmark] public int Calli() => _calli(_calculator, 1, 2); }
如上所示的測試程序很簡單,我們調用CreateInvoker方法將針對三種指令的Func<Calculator, int, int, int>委托和目標對象FakeCalculator創(chuàng)建出來,并在三個Benchmark方法中執(zhí)行它們。從如下的測試結果可以看出,Call由于不需要進行”虛方法分發(fā)(Virtual Dispatch)”性能會比Callvirt執(zhí)行好一些,但總體來說差別不大,但是Calli指令調用方法的性能會差很多。


在《可以調用Null的實例方法嗎?》一文中,我談到.NET方法的三種調用形式,現在我們就來著重聊聊這個話題。具體來說,這里所謂的三種方法調用形式對應著三種IL指令:Call、CallVirt和Calli。


浙公網安備 33010602011771號