我有一个比较两个可为null的int并将比较结果打印到控制台的方法:

static void TestMethod(int? i1, int? i2)
{
    Console.WriteLine(i1 == i2);
}

这是其反编译的结果:
private static void TestMethod(int? i1, int? i2)
{
    int? nullable = i1;
    int? nullable2 = i2;
    Console.WriteLine((nullable.GetValueOrDefault() == nullable2.GetValueOrDefault()) & (nullable.HasValue == nullable2.HasValue));
}

结果或多或少是我所期望的,但我想知道为什么使用逻辑'and'运算符(&)的非短路版本而不是短路版本(&&)。在我看来,后者会更有效-如果已知比较的一侧是错误的,则无需评估另一侧。在这里是否需要&运算符,或者这仅仅是一个实现细节,其重要性还不至于令人困扰?

最佳答案



这是一个很好的问题。

首先,我为Roslyn之前的可为空的降低代码以及Roslyn中的原始实现开发了代码生成器。这是一些棘手的代码,有很多机会出错和错过优化。我写了很多博客文章,介绍Roslyn可为空的降低优化器的工作原理,从这里开始:

https://ericlippert.com/2012/12/20/nullable-micro-optimizations-part-one/

如果您对此主题感兴趣,那么这一系列文章可能会大有帮助。第三部分特别贴切,因为它讨论了一个相关的问题:对于可为空的算术,我们生成(x.HasValue & y.HasValue) ? new int?(x.Value + y.Value) : new int?()还是使用&&或使用GetValueOrDefault还是什么? (当然,答案是我尝试了所有方法,然后选择了使最快的最小代码成为可能的方法。)但是,该系列文章在这里不考虑您的特定问题,即关于可空相等性的问题。可空相等的规则与普通提升算术的规则略有不同。

当然,自2012年以来我就没有去过Microsoft,并且从那时起他们可能已经改变了它。我不知道(更新:查看上面评论中的链接问题,似乎我错过了2011年最初实现的优化,而该优化已在2017年修复。)

要回答您的特定问题:&&的问题在于,当在运算符(operator)右侧进行的工作比测试和分支便宜时,它比&贵。测试和分支不仅仅是更多的说明。显然,它是一个分支,具有许多链式效应。在处理器级别,分支需要分支预测,并且分支可能被错误预测。分支意味着更多的基本块,请记住,jit优化器在运行时运行,这意味着jit优化器必须快速。可以说“此方法中的基本块太多,我将跳过一些优化”,因此也许不必要地添加更多基本块是一件坏事。

长话短说,如果右侧没有副作用,C#编译器将急切地生成“和”操作,并且编译器认为评估left & right比评估left ? right : false更快,更短。通常,评估right的成本如此便宜,以至于分支的成本比仅仅执行急切的算术还要昂贵。

您可以在可为空的优化器之外的其他区域看到此内容。例如,如果您有

bool x = X();
bool y = Y();
bool z = x && y;

然后将其生成为z = x & y,因为编译器知道无需保存任何昂贵的操作; Y()已经被调用。

10-07 21:55