问题 如果新的c#6“?”null检查,则调用而不是callvirt


鉴于这两种方法:

    static void M1(Person p)
    {
        if (p != null)
        {
            var p1 = p.Name;
        }
    }

    static void M2(Person p)
    {
        var p1 = p?.Name;
    }

为什么M1 IL代码使用 callvirt

IL_0007:  brfalse.s  IL_0012
IL_0009:  nop
IL_000a:  ldarg.0
IL_000b:  callvirt   instance string ConsoleApplication4.Person::get_Name()

和M2 IL使用 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()

我只能猜到它,因为在M2中我们知道这一点 p 不是空的,就像它一样

new MyClass().MyMethod();

这是真的吗?

如果是的话,如果 p 在其他线程中将为null?


6206
2017-12-30 18:24


起源

如果显示在重写时如何调用实际的虚拟成员将会很高兴。特别是在场的情况下 ?.。被覆盖的成员是什么行为 sealed 并特别援引?如果它仍然存在 callvirt,这可能是罗斯林的优化机会:) - leppie
@leppie C#generate callvirt 为了这。但我不知道运行时发生了什么。 - Dudi Keleti


答案:


callvirt 在M1是 标准C#代码生成。它提供了语言保证,即永远不能使用空引用调用实例方法。换句话说,它确保了 p != null 如果为null,则生成NullReferenceException。您的明确测试不会改变这一点。

这个保证非常好,调试NRE如果它变得非常毛茸茸 this 这是null。反而更容易诊断呼叫站点的事故,调试器可以快速向您显示它 p 这是麻烦制造者。

但是当然 callvirt 不是免费的,虽然成本非常低,在运行时一个额外的处理器指令。所以,如果它 能够 被...代替 call 然后代码将快半个纳秒,给予或采取。它实际上可以与elvis运算符一起使用,因为它已经确保引用不为null,因此C#6编译器利用了它并生成调用而不是callvirt。


7
2017-12-31 17:09



谢谢你的详细信息。我在上面的链接中写了一篇文章,但我不知道大约半个纳秒:) - Dudi Keleti


我认为现在很清楚,

在触发事件之前,这是一种检查null的简单且线程安全的方法。它是线程安全的原因是该功能仅评估左侧一次,并将其保存在临时变量中。 MSDN

所以使用起来是安全的 call 在这里指导。

我写了一篇 博客文章 关于之间的差异 call 和 callvirt 以及为什么C#生成 callvirt

谢谢 丹里昂斯 用于MSDN链接。


5
2017-12-30 18:47





从事实开始 callvirt 用来代替 call 因为C#规则,即使.NET允许,空对象也可能没有调用它们的方法。

现在,在你的两种方法中,我们都可以静态地显示出来 p 不是null,因此使用 call 代替 callvirt 不会打破这个C#规则,因此这是一个合理的优化。

if (a != null) a.b 等等是一种常见的习语,需要通过分析来实现 a 在那一点上不能为空 b 用来。将该分析添加到编译器将需要工作规范,实现,测试和持续测试其他更改引入的回归错误。

a?.b 超出成语,因为它使用的是运算符 ?. C#必须“知道”。所以C#必须有代码将其转换为空检查,然后是成员访问。所以编译器必须知道在成员访问发生时, a 不是空的。正如这样的逻辑“知道”使用 call 安全已经完成。实现这一目标并不是额外的分析工作 call 可以使用。

所以第一种情况需要一堆额外的工作才能使用 call并且可能会引入错误,而第二种情况无论如何都要做这项工作,所以它也可能。


1
2018-01-11 15:32