?.Invoke(x) 的性能是否与空检查 + 直接调用一样好
Does ?.Invoke(x) perform as well as null-check + direct call
当你有这样的陈述时,Visual Studio 抱怨 "Delegate invocation can be simplified":
Action<int> foo = x;
if (foo != null)
foo(10);
快速操作智能标记规则希望您将其更改为:
Action<int> foo = x;
foo?.Invoke(10);
编译器是否以一种很好的方式为您处理这个问题并以两种方式生成相同的代码?还是后者表现不同?
在关闭优化的构建中(通常是调试构建),您将获得以下两个 IL 指令序列:
IL_0000: nop IL_0000: nop
IL_0001: ldnull IL_0001: ldnull
IL_0002: ldftn x IL_0002: ldftn x
IL_0008: newobj Action<int>..ctor IL_0008: newobj Action<int>..ctor
IL_000D: stloc.0 // foo IL_000D: stloc.0 // foo
IL_000E: ldloc.0 // foo IL_000E: ldloc.0 // foo
IL_000F: ldnull IL_000F: brtrue.s IL_0013
IL_0010: cgt.un IL_0011: br.s IL_001C
IL_0012: stloc.1
IL_0013: ldloc.1
IL_0014: brfalse.s IL_001F
IL_0016: ldloc.0 // foo IL_0013: ldloc.0 // foo
IL_0017: ldc.i4.s 0A IL_0014: ldc.i4.s 0A
IL_0019: callvirt Action<int>.Invoke IL_0016: callvirt Action<int>.Invoke
IL_001E: nop IL_001B: nop
IL_001F: ret IL_001C: ret
这里关于分支指令的细微差别,但让我们在打开优化的情况下构建(通常是发布构建):
IL_0000: ldnull IL_0000: ldnull
IL_0001: ldftn x IL_0001: ldftn x
IL_0007: newobj Action<int>..ctor IL_0007: newobj Action<int>..ctor
IL_000C: stloc.0 // foo IL_000C: dup
IL_000D: ldloc.0 // foo IL_000D: brtrue.s IL_0011
IL_000E: brfalse.s IL_0018 IL_000F: pop
IL_0010: ldloc.0 // foo IL_0010: ret
IL_0011: ldc.i4.s 0A IL_0011: ldc.i4.s 0A
IL_0013: callvirt Action<int>.Invoke IL_0013: callvirt Action<int>.Invoke
IL_0018: ret IL_0018: ret
同样,分支指令略有不同。具体来说,使用空合并运算符的示例会将操作委托引用的副本压入堆栈,而带有 if 语句的示例将使用临时局部变量。 JITter 可能会将两者都放入寄存器中,但是,这并不能断定它的行为会有所不同。
让我们尝试一些不同的东西:
public static void Action1(Action<int> foo)
{
if (foo != null)
foo(10);
}
public static void Action2(Action<int> foo)
{
foo?.Invoke(10);
}
这被编译(同样,打开优化)为:
IL_0000: ldarg.0 IL_0000: ldarg.0
IL_0001: brfalse.s IL_000B IL_0001: brfalse.s IL_000B
IL_0003: ldarg.0 IL_0003: ldarg.0
IL_0004: ldc.i4.s 0A IL_0004: ldc.i4.s 0A
IL_0006: callvirt Action<int>.Invoke IL_0006: callvirt Action<int>.Invoke
IL_000B: ret IL_000B: ret
完全相同的代码。因此,上述示例中的差异是由于 null 合并运算符以外的其他因素而不同。
现在,回答您的具体问题,分支顺序与您的示例的差异是否会影响性能? 了解这一点的唯一方法是实际进行基准测试。 但是,如果它出现,我会非常感到惊讶原来是你需要考虑的事情。相反,我会根据您认为最容易编写、阅读和理解的内容来选择代码风格。
Visual Studio 抱怨 "Delegate invocation can be simplified":
Action<int> foo = x;
if (foo != null)
foo(10);
快速操作智能标记规则希望您将其更改为:
Action<int> foo = x;
foo?.Invoke(10);
编译器是否以一种很好的方式为您处理这个问题并以两种方式生成相同的代码?还是后者表现不同?
在关闭优化的构建中(通常是调试构建),您将获得以下两个 IL 指令序列:
IL_0000: nop IL_0000: nop
IL_0001: ldnull IL_0001: ldnull
IL_0002: ldftn x IL_0002: ldftn x
IL_0008: newobj Action<int>..ctor IL_0008: newobj Action<int>..ctor
IL_000D: stloc.0 // foo IL_000D: stloc.0 // foo
IL_000E: ldloc.0 // foo IL_000E: ldloc.0 // foo
IL_000F: ldnull IL_000F: brtrue.s IL_0013
IL_0010: cgt.un IL_0011: br.s IL_001C
IL_0012: stloc.1
IL_0013: ldloc.1
IL_0014: brfalse.s IL_001F
IL_0016: ldloc.0 // foo IL_0013: ldloc.0 // foo
IL_0017: ldc.i4.s 0A IL_0014: ldc.i4.s 0A
IL_0019: callvirt Action<int>.Invoke IL_0016: callvirt Action<int>.Invoke
IL_001E: nop IL_001B: nop
IL_001F: ret IL_001C: ret
这里关于分支指令的细微差别,但让我们在打开优化的情况下构建(通常是发布构建):
IL_0000: ldnull IL_0000: ldnull
IL_0001: ldftn x IL_0001: ldftn x
IL_0007: newobj Action<int>..ctor IL_0007: newobj Action<int>..ctor
IL_000C: stloc.0 // foo IL_000C: dup
IL_000D: ldloc.0 // foo IL_000D: brtrue.s IL_0011
IL_000E: brfalse.s IL_0018 IL_000F: pop
IL_0010: ldloc.0 // foo IL_0010: ret
IL_0011: ldc.i4.s 0A IL_0011: ldc.i4.s 0A
IL_0013: callvirt Action<int>.Invoke IL_0013: callvirt Action<int>.Invoke
IL_0018: ret IL_0018: ret
同样,分支指令略有不同。具体来说,使用空合并运算符的示例会将操作委托引用的副本压入堆栈,而带有 if 语句的示例将使用临时局部变量。 JITter 可能会将两者都放入寄存器中,但是,这并不能断定它的行为会有所不同。
让我们尝试一些不同的东西:
public static void Action1(Action<int> foo)
{
if (foo != null)
foo(10);
}
public static void Action2(Action<int> foo)
{
foo?.Invoke(10);
}
这被编译(同样,打开优化)为:
IL_0000: ldarg.0 IL_0000: ldarg.0
IL_0001: brfalse.s IL_000B IL_0001: brfalse.s IL_000B
IL_0003: ldarg.0 IL_0003: ldarg.0
IL_0004: ldc.i4.s 0A IL_0004: ldc.i4.s 0A
IL_0006: callvirt Action<int>.Invoke IL_0006: callvirt Action<int>.Invoke
IL_000B: ret IL_000B: ret
完全相同的代码。因此,上述示例中的差异是由于 null 合并运算符以外的其他因素而不同。
现在,回答您的具体问题,分支顺序与您的示例的差异是否会影响性能? 了解这一点的唯一方法是实际进行基准测试。 但是,如果它出现,我会非常感到惊讶原来是你需要考虑的事情。相反,我会根据您认为最容易编写、阅读和理解的内容来选择代码风格。