Search code examples
c#performanceinvocation

Does ?.Invoke(x) perform as well as null-check + direct call


Visual Studio bleats about "Delegate invocation can be simplified" when you have statements such as this:

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

The quick action smart tag rules want you to change this to:

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

Does the compiler deal with this for you in a nice way and produce the same code either way? Or does the latter perform differently?


Solution

  • In build with optimization turned off (typically Debug builds) you will get the following two IL instruction sequences:

    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 
    

    Slight differences here regarding the branch instructions, but let's build with optimizations turned on (typically Release builds):

    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 
    

    Again, slight difference in the branch instructions. Specifically, the example using a null-coalescing operator will push a duplicate of the action delegate reference on the stack, whereas the one with the if-statement will use a temporary local variable. The JITter might put both into a register, however, this isn't conclusive that it will behave differently.

    Let's try something different:

    public static void Action1(Action<int> foo)
    {
        if (foo != null)
            foo(10);
    }
    
    public static void Action2(Action<int> foo)
    {
        foo?.Invoke(10);
    }
    

    This gets compiled (again, with optimizations turned on) to:

    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  
    

    The exact same code. So the differences in the above examples were different because of other things than the null-coalescing operator.

    Now, to answer your specific question, will the branch sequence differences from your example impact performance? The only way to know this is to actually benchmark. However, I would be very surprised if it turned out to be something you need to take into account. Instead I would choose the style of code depending on what you find easiest to write, read, and understand.