0

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?

Rob
  • 1,687
  • 3
  • 22
  • 34
  • 1
    It does the same. – György Kőszeg Oct 25 '18 at 11:32
  • 2
    Everything is written in documentation [Null-conditional Operator](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/null-conditional-operators) – Renatas M. Oct 25 '18 at 11:32
  • 1
    This was answered before. 10 sec google yields this https://stackoverflow.com/questions/49853515/null-condition-and-null-coalescing-operator-vs-plain-boolean-notation – Csharpest Oct 25 '18 at 11:32
  • It's more the .Invoke() I was concerned about not the null-coalescing, and or the interaction with them both. – Rob Oct 25 '18 at 11:34
  • 1
    Yes, it deals with it "in a nice way", but it does not produce the exact same code -- the IL sequences for both approaches are slightly different. (This, of course, is subject to change in any version as long as the semantics are respected.) Does it perform differently? Improbable, but you could benchmark if you had reason to care. – Jeroen Mostert Oct 25 '18 at 11:35
  • @Rob: The `Invoke` is implicitly there also in the first case. You just can't write `foo?.(10)`, that's why you must explicitly write `Invoke` when dealing with delegates in terms of the null-conditional operator. – György Kőszeg Oct 25 '18 at 11:38
  • @Fildor: not really, as the *real* answer would involve notes on how you find this out for yourself, how you do the perf tests if you felt you had to, and generally be more effort than I want to expend writing down. "Here's how my C# compiler happens to do it right now" is *an* answer, not *the* answer. (And not the kind of answer I really want on my permanent record.) – Jeroen Mostert Oct 25 '18 at 11:52
  • @JeroenMostert Fair enough. – Fildor Oct 25 '18 at 11:54
  • If I recall, one of the design goals of the Null Coalescing Operator was to explicitly handle this pattern of delegate invocation, and simplify it, without losing any of the thread-safety. As far as performance goes, it may actually perform *faster*. Bench marking would be needed to verify either way though. – Bradley Uffner Oct 25 '18 at 12:21
  • The two will produce identical IL in release build so no, there will not be any differences. – Lasse V. Karlsen Oct 25 '18 at 12:40
  • I can't believe I forgot to ask, but is there any reason you don't just write the second example as `x?.Invoke(10)`? Avoiding a temporary just to get thread safety is exactly one of the benefits of the new pattern. – Jeroen Mostert Oct 25 '18 at 12:59
  • @JeroenMostert no reason, it was just to show typing for foo in an real-world example I was using locally. It was the null-coalescing + invoke combination I was targeting. – Rob Oct 25 '18 at 14:10

1 Answers1

3

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.

Lasse V. Karlsen
  • 380,855
  • 102
  • 628
  • 825
  • Great answer, thank you for the obvious effort. It certainly looks as though the compiled results are not worth worrying about. That being so, I'd agree with your final statement. It's nice to know I can switch to the latter invocation method, as I personally prefer the one-liner. – Rob Oct 25 '18 at 14:13