# 前言
在 《CLR vir C#》一书中,有说明 call
与 callvirt
的差别:
- call:可调用静态方法,实例方法和虚方法
- 调用实例方法和虚方法必须指定引用了对象的变量,该指令假定该变量不为 null
- callvirt:可调用实例方法和虚方法,不能调用静态方法(需要对变量做 null 检查,因此比 call 慢)
- 并且调用虚实例方法时,还需要检查发出调用对象的实际类型,然后以多态方式调用
同时,对于声明为 sealed
的类型,会有优化,采用 call
调用,例如:始终对结构体采用 call 调用。
这里我想进行各种调用测试,并从 IL
代码上确认下。
# 工具
- ILSpy 版本 8.0.0.7246-preview3
# 测试类型 (普通方法)
private class ClassNormal | |
{ | |
public void InvokeNormal() { } | |
} | |
private sealed class ClassSealed | |
{ | |
public void InvokeSealed() { } | |
} | |
private struct StructNormal | |
{ | |
public void InvokeStruct() { } | |
} |
# 调用方式一
直接 new
并立即调用:
new ClassNormal().InvokeNormal(); | |
new ClassSealed().InvokeSealed(); | |
new StructNormal().InvokeStruct(); |
IL 代码:
IL_0008: newobj instance void TestCallMethod/ClassNormal::.ctor() | |
IL_000d: call instance void TestCallMethod/ClassNormal::InvokeNormal() | |
IL_0012: nop | |
IL_0013: newobj instance void TestCallMethod/ClassSealed::.ctor() | |
IL_0018: call instance void TestCallMethod/ClassSealed::InvokeSealed() | |
IL_001d: nop | |
IL_001e: ldloca.s 0 | |
IL_0020: dup | |
IL_0021: initobj TestCallMethod/StructNormal | |
IL_0027: call instance void TestCallMethod/StructNormal::InvokeStruct() |
直接 new
并立即调用普通方法,三者并无却别,均编译为 call
指令 —— 因为编译器知道调用对象必然不为空,无需进行额外检查。
# 调用方式二
缓存调用:
ClassNormal normal = new ClassNormal(); | |
normal.InvokeNormal(); | |
ClassSealed sl = new ClassSealed(); | |
sl.InvokeSealed(); | |
StructNormal st = new StructNormal(); | |
st.InvokeStruct(); |
IL 代码:
IL_0008: newobj instance void TestCallMethod/ClassNormal::.ctor() | |
IL_000d: stloc.0 | |
IL_000e: ldloc.0 | |
IL_000f: callvirt instance void TestCallMethod/ClassNormal::InvokeNormal() | |
IL_0014: nop | |
IL_0015: newobj instance void TestCallMethod/ClassSealed::.ctor() | |
IL_001a: stloc.1 | |
IL_001b: ldloc.1 | |
IL_001c: callvirt instance void TestCallMethod/ClassSealed::InvokeSealed() | |
IL_0021: nop | |
IL_0022: ldloca.s 2 | |
IL_0024: initobj TestCallMethod/StructNormal | |
IL_002a: ldloca.s 2 | |
IL_002c: call instance void TestCallMethod/StructNormal::InvokeStruct() |
结构体调用依然为 call
指令,类型调用已经换成了 callvirt
指令 —— 调用普通类型需要额外检查了。
# 测试类型 (虚方法)
private class ClassVirtual | |
{ | |
public virtual void Invoke() { } | |
} | |
private class ClassVirtualChild : ClassVirtual | |
{ | |
public override void Invoke() { } | |
} | |
private sealed class ClassVirtualChildSealed : ClassVirtual | |
{ | |
public override void Invoke() { } | |
} |
# 调用方式一
直接 new
调用:
new ClassVirtualChild().Invoke(); | |
new ClassVirtualChildSealed().Invoke(); |
IL 代码:
IL_0008: newobj instance void TestCallMethod/ClassVirtualChild::.ctor() | |
IL_000d: callvirt instance void TestCallMethod/ClassVirtual::Invoke() | |
IL_0012: nop | |
IL_0013: newobj instance void TestCallMethod/ClassVirtualChildSealed::.ctor() | |
IL_0018: callvirt instance void TestCallMethod/ClassVirtual::Invoke() |
... 貌似 sealed
没起到作用,对于类型虚方法调用还是采用的 callvirt
指令调用的基类方法名
# 调用方式二
缓存调用:
ClassVirtualChild normal = new ClassVirtualChild(); | |
normal.Invoke(); | |
ClassVirtualChildSealed sl = new ClassVirtualChildSealed(); | |
sl.Invoke(); |
IL 代码:
IL_0008: newobj instance void TestCallMethod/ClassVirtualChild::.ctor() | |
IL_000d: stloc.0 | |
IL_000e: ldloc.0 | |
IL_000f: callvirt instance void TestCallMethod/ClassVirtual::Invoke() | |
IL_0014: nop | |
IL_0015: newobj instance void TestCallMethod/ClassVirtualChildSealed::.ctor() | |
IL_001a: stloc.1 | |
IL_001b: ldloc.1 | |
IL_001c: callvirt instance void TestCallMethod/ClassVirtual::Invoke() |
与第一种方式一样, sealed
标识之后,也还是始终采用 callvirt
调用基类方法名
# 测试类型 - 静态调用
根据各种信息表明,静态函数必然是 call
指令调用,不过最后还是测试一下:
private class ClassVirtualChild : ClassVirtual | |
{ | |
public static void InvokeStatic() { } | |
} | |
ClassVirtualChild.InvokeStatic(); |
IL 代码:
IL_0008: call void TestCallMethod/ClassVirtualChild::InvokeStatic() |
确实如此。
# MethodImplOptions.AggressiveInlining 内联特性测试
据说将方法使用 [MethodImpl(MethodImplOptions.AggressiveInlining)]
特性标记, CLR
如可能会将方法内联。
—— 虽然好像说是 JIT
生效的,不过这里也还是做一下测试,看看讷会不会体现在 IL
中:
private class ClassVirtual | |
{ | |
public virtual void Invoke() { } | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public void InvokeDirect() { Debug.Log("InvokeDirect"); } | |
} | |
private class ClassVirtualChild : ClassVirtual | |
{ | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public override void Invoke() { Debug.Log("ClassVirtualChild"); } | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public void InvokeNormal() { Debug.Log("InvokeNormal"); } | |
} | |
private sealed class ClassVirtualChildSealed : ClassVirtual | |
{ | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public sealed override void Invoke() { Debug.Log("ClassVirtualChildSealed"); } | |
} | |
new ClassVirtual().InvokeDirect(); | |
new ClassVirtualChild().Invoke(); | |
new ClassVirtualChild().InvokeNormal(); | |
new ClassVirtualChildSealed().Invoke(); | |
ClassVirtualChild normal = new ClassVirtualChild(); | |
normal.Invoke(); | |
normal.InvokeNormal(); | |
ClassVirtualChildSealed sl = new ClassVirtualChildSealed(); | |
sl.Invoke(); |
IL 代码 (优化版):
IL_0006: newobj instance void TestCallMethod/ClassVirtual::.ctor() | |
IL_000b: call instance void TestCallMethod/ClassVirtual::InvokeDirect() | |
IL_0010: newobj instance void TestCallMethod/ClassVirtualChild::.ctor() | |
IL_0015: callvirt instance void TestCallMethod/ClassVirtual::Invoke() | |
IL_001a: newobj instance void TestCallMethod/ClassVirtualChild::.ctor() | |
IL_001f: call instance void TestCallMethod/ClassVirtualChild::InvokeNormal() | |
IL_0024: newobj instance void TestCallMethod/ClassVirtualChildSealed::.ctor() | |
IL_0029: callvirt instance void TestCallMethod/ClassVirtual::Invoke() | |
IL_002e: newobj instance void TestCallMethod/ClassVirtualChild::.ctor() | |
IL_0033: dup | |
IL_0034: callvirt instance void TestCallMethod/ClassVirtual::Invoke() | |
IL_0039: callvirt instance void TestCallMethod/ClassVirtualChild::InvokeNormal() | |
IL_003e: newobj instance void TestCallMethod/ClassVirtualChildSealed::.ctor() | |
IL_0043: callvirt instance void TestCallMethod/ClassVirtual::Invoke() |
所以貌似没有体现在 IL
代码中,估计该特性真得 JIT
的时候才可能生效,也就是说还是得看 CLR
的判断了,毕竟本身该特性也并未注明一定会对方法产生内联。
注:另外根据 methodimpl-methodimploptions-aggressiveinlining 文章的相关信息所说,
IL2CPP
和Mono standalone
支持的,不过编辑器内不会体现出来。
# 总结
- 结构体为值类型,且不支持继承,因此调用其实例方法时直接采用
call
指令,不必进行额外空判断。 - 引用类型调用,普通实例方法在直接
new X.Method()
采用call
指令,其它方式则callvirt
方式。 - 引用类型虚方法调用,仅
callvirt
方式调用基类方法- 即:调用派生类重载的虚方法,都是通过
callvirt
调用基类方法
- 即:调用派生类重载的虚方法,都是通过
所以经过测试,可以得出调用方式结论:
- 静态函数始终为
call
调用 - 结构体方法使用为
call
调用 - 普通方法仅
new X.Method()
方式采用call
调用 - 虚方法通过
callvirt
基类方法进行调用- 可能还是在传递实例后,由基类方法那边再进行额外多态判断?(书里确实也是这样说的)
- sealed 就算有优化也是处于
JIT
时,毕竟并未改变il
编译出来的调用指令 (当然理论上应该有,因为不用判断调用实例类型的子类了)
还有信息表示: call
调用申明类型方法, callvirt
调用变量指向对象的实际类型方法
更多可参考文章:用 MSIL 写程序:从 “call vs callvirt” 看方法调用
这里只是了解了 C#
编译为 IL
时各方法的调用方式,按照相关信息, JIT
时还会有额外优化,例如说上面提到的 sealed
我们本身并不能控制什么时候必须用 call
指令,除非自己写 IL
代码,不过了解了各种情况编译结果,至少能更明白这些调用的差异。
其它:
- 官方文档 - call
- 官方文档 - callvirt