Given the two methods:
static void M1(Person p)
{
if (p != null)
{
var p1 = p.Name;
}
}
static void M2(Person p)
{
var p1 = p?.Name;
}
Why the M1 IL code use callvirt
:
IL_0007: brfalse.s IL_0012
IL_0009: nop
IL_000a: ldarg.0
IL_000b: callvirt instance string ConsoleApplication4.Person::get_Name()
and the M2 IL use call
:
brtrue.s IL_0007
IL_0004: ldnull
IL_0005: br.s IL_000d
IL_0007: ldarg.0
IL_0008: call instance string ConsoleApplication4.Person::get_Name()
I just can guess that it because in M2 we know that p
isn't null and its like
new MyClass().MyMethod();
Is it true?
If it is, what if p
will be null in other thread?
The callvirt
in M1 is standard C# code generation. It provides the language guarantee that an instance method can never be called with a null reference. In other words, it ensures that p != null
and generates NullReferenceException if it is null. Your explicit test does not change that.
This guarantee is pretty nice, debugging NRE gets pretty hairy if it is this
that is null. Much easier to diagnose the mishap at the call-site instead, the debugger can quickly show you that it is p
that is the troublemaker.
But of course callvirt
is not for free, although the cost is very low, one extra processor instruction at runtime. So if it can be substituted by call
then the code will be faster by half a nanosecond, give or take. It in fact can with the elvis operator since it already ensures that the reference isn't null so the C# 6 compiler took advantage of that and generates call instead of callvirt.
I think it's clearly now,
This is an easy and thread-safe way to check for null before you trigger an event. The reason it’s thread-safe is that the feature evaluates the left-hand side only once, and keeps it in a temporary variable. MSDN
So it is safe to use call
instruction here.
I wrote a blog post about the differences between call
and callvirt
and why C# generate callvirt
Thanks Dan Lyons for the MSDN link.
Start with the fact that callvirt
is used instead of call
because of the C# rule that null objects may not have methods called on them, even when .NET allows it.
Now, in both your methods, we can show statically that p
is not null, and as such using call
instead of callvirt
is not going to break this C# rule, and as such is a reasonable optimisation.
While if (a != null) a.b
etc. is a common idiom, it takes analysis to realise that a
cannot be null at the point that b
is used. Adding that analysis to the compiler would take work specing, implementing, testing, and continually testing against regression bugs introduced by other changes.
a?.b
is beyond an idiom, in that it's using an operator ?.
that C# must "know" about. So C# has to have the code to turn this into a null check followed by a member access. So the compiler has to know that at the point where the member access happens, a
is not null. As such the logic to "know" the use of call
is safe has already been done. It's no extra analysis work to realise that call
can be used.
So the first case would require a bunch of extra work to use call
and potentially introduce bugs, while the second case has to do that work anyway, so it might as well.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With