?.Invoke(x)的执行与空检查+直接调用一样

时间:2018-10-25 11:30:08

标签: c# performance invocation

当您具有如下语句时,Visual Studio会抱怨“可以简化代理调用”:

Action<int> foo = x;
if (foo != null)
    foo(10);

快速行动智能标记规则希望您将其更改为:

Action<int> foo = x;
foo?.Invoke(10);

编译器是否以一种不错的方式为您处理此问题,并且以任何一种方式生成相同的代码?还是后者的表现有所不同?

1 个答案:

答案 0 :(得分:3)

在关闭优化的构建中(通常是Debug构建),您将获得以下两个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 

此处关于分支指令的差异略有不同,但让我们在启用优化的情况下进行构建(通常是Release版本):

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 
再次,分支指令中的细微差别。具体来说,使用null运算符的示例将把动作委托引用的副本推送到堆栈上,而带有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运算符之外的其他原因而导致的。

现在,要回答您的特定问题,分支序列与示例中的差异是否会影响性能? 知道这一点的唯一方法是实际进行基准测试。 但是,如果事实证明这是您需要采取的措施,我会非常感到惊讶考虑在内。相反,我会根据您发现最容易编写,阅读和理解的内容来选择代码的样式。