call instead of callvirt in case of the new c# 6 “

2019-04-19 15:10发布

问题:

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?

回答1:

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.



回答2:

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.



回答3:

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.