# 前言

在 《CLR vir C#》一书中,有说明 callcallvirt 的差别:

  • 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 文章的相关信息所说, IL2CPPMono 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