【问题标题】:Reference equality performance difference? ((object)obj1 == (object)obj2) vs. object.ReferenceEquals( obj1, obj2 )引用相等性能差异? ((object)obj1 == (object)obj2) 与 object.ReferenceEquals(obj1, obj2)
【发布时间】:2010-10-18 15:28:53
【问题描述】:

使用object.ReferenceEquals 方法与使用((object)obj1 == (object)obj2) 相比是否有额外的开销?

在第一种情况下,会涉及到静态方法调用,并且在这两种情况下都会涉及到对象的某种形式的强制转换。

即使编译器平衡了这些方法,不等式又如何呢?

(object)obj != null

相比...

!object.ReferenceEquals(obj,null)

我想在某些时候,会发生逻辑否定,或者在 != 运算符中,或者应用于 ReferenceEquals 方法的结果。你怎么看?

还有可读性问题需要考虑。 ReferenceEquals 在检查相等性时似乎更清晰,但对于不等式,人们可能会错过 object.ReferenceEquals 之前的 !,而第一个变体中的 != 很难忽视.

【问题讨论】:

    标签: c# performance coding-style equality readability


    【解决方案1】:

    使用 object.ReferenceEquals 方法是否有额外开销

    没有。该方法直接包含执行引用相等检查的最小 IL 描述(记录:它相当于 VB 的 Is 运算符)并且通常会被 JIT 内联(尤其是针对 x64 时),所以没有 no强>开销。

    关于可读性:我个人认为object.ReferenceEquals 可能更具可读性(即使是否定形式),因为它明确地表达了它的语义。 object 的转换可能会让一些程序员感到困惑。

    我刚刚发现 an article 正在讨论这个问题。它更喜欢(object)x == y,因为 IL 占用空间更小。它认为这可能有助于使用这种比较方法内联方法X。但是(没有任何关于 JIT 的详细知识,但在逻辑上和直观上)我认为这是错误的:如果 JIT 的行为类似于优化 C++ 编译器,它会考虑将方法 after 内联对 @987654327 的调用@,所以(为了内联方法X)内存占用将完全相同。

    也就是说:选择一种方式而不是另一种方式对 JIT 并因此对性能没有任何影响。

    【讨论】:

    • 我同意对象的转换令人困惑。有人可能想知道为什么需要在 ((object)obj == null) 中进行强制转换,但没有意识到引用相等最终归结为 (object == object)。此外,与 null 的比较似乎隐含着引用比较,但实际上选择了类型化运算符。
    • 不一定是内联的。取决于 JIT 对特定代码集的感觉。我肯定看到它不是内联的。这适用于许多小型辅助函数。根据您在做什么,它可能会慢很多。在循环中更有可能被内联,但不能保证。
    • @Celess 这更取决​​于使用的 JIT:x64 JIT 应该内联它,x86 JIT 可能不会(不幸的是)因为即使是最明显的情况也不愿意内联。但你的警告是正确的,所以我更新了答案以限定陈述。
    • 您引用的文章已过时。在 .NET 4.0(我检查了 4.6)中,生成的 IL 在两种方法之间是逐字节相等的。对ReferenceEquals 的方法调用甚至不再出现在 IL 中。
    【解决方案2】:

    与此处的答案相反,我发现 (object) ==object.ReferenceEquals 快​​。至于有多快,可以忽略不计!

    试验台:

    我知道您需要进行引用相等性检查,但我也包括静态 object.Equals(,) 方法,以防它未被覆盖的类。

    平台:x86;配置:发布构建

    class Person {
    }
    
    public static void Benchmark(Action method, int iterations = 10000)
    {
        Stopwatch sw = new Stopwatch();
        sw.Start();
        for (int i = 0; i < iterations; i++)
            method();
    
        sw.Stop();
        MsgBox.ShowDialog(sw.Elapsed.TotalMilliseconds.ToString());
    }
    

    测试:

    Person p1 = new Person();
    Person p2 = new Person();
    bool b;
    Benchmark(() =>
    {
        b = (object)p1 == (object)p2; //960 ~ 1000ms
        b = object.ReferenceEquals(p1, p2); //~ 1250ms
        b = object.Equals(p1, p2); //2100ms
        b = EqualityComparer<Person>.Default.Equals(p1, p2); //~4000ms
    
    }, 100000000);
    
    Person p1 = new Person();
    Person p2 = null;
    bool b;
    Benchmark(() =>
    {
        b = (object)p1 == (object)p2; //990 ~ 1000ms
        b = object.ReferenceEquals(p1, p2); // 1230 ~ 1260ms
        b = object.Equals(p1, p2); //1250 ~ 1300ms
        b = EqualityComparer<Person>.Default.Equals(p1, p2); //~3100ms
    
    }, 100000000);
    
    Person p1 = null;
    Person p2 = null;
    bool b;
    Benchmark(() =>
    {
        b = (object)p1 == (object)p2; //960 ~ 1000ms
        b = object.ReferenceEquals(p1, p2); //1260 ~ 1270ms
        b = object.Equals(p1, p2); //1180 ~ 1220ms
        b = EqualityComparer<Person>.Default.Equals(p1, p2); //~3100ms
    
    }, 100000000);
    
    Person p1 = new Person();
    Person p2 = p1;
    bool b;
    Benchmark(() =>
    {
        b = (object)p1 == (object)p2; //960 ~ 1000ms
        b = object.ReferenceEquals(p1, p2); //1260 ~ 1280ms
        b = object.Equals(p1, p2); //1150 ~ 1200ms
        b = EqualityComparer<Person>.Default.Equals(p1, p2); //3700 ~ 3800ms
    
    }, 100000000);
    

    object.Equals(,) 在内部调用ReferenceEquals,如果它们不相等,它将调用类的覆盖虚拟Equals 方法,因此您会看到速度差异的通知。

    Debug 配置中的结果也是一致的...

    正如所指出的,重点应放在可读性/意义/揭示意图上。

    【讨论】:

    • 与几乎所有已发布的微基准测试一样,您没有考虑样本的方差。适当的微基准需要包括统计显着性分析,否则整个结果可能是侥幸。仅仅重复测试几次是不够的。此外,为了在这里获得权威答案,我们应该查看生成的代码(不是 IL,而是 JIT 生成的机器代码),这将向我们展示我们需要知道的一切。问题是 x86 JIT 实际上并没有执行很多内联,所以你的结果是合理的。 (在 x64 上,结果应该不同。)
    • 这不是我所说的方差。我的意思是狭义统计中的variance,特别是在statistical significance testing 的上下文中。请注意,我仍然认为这是一个很好的答案,因此 +1。
    • @nawfal 我们可以假设所有这些的否定都是相同的((object)p1 != (object)p2)吗?我认为不,所以在这些上运行测试可能会很酷...... :)
    【解决方案3】:

    Object.ReferenceEquals 的开销仅在于加载参数,在大多数情况下会被 JIT 删除。之后, Object.ReferenceEquals 和 operator== 都归结为一个 IL ceq 运算符。在最坏的情况下,差异将是微不足道的。

    更重要的是,Object.ReferenceEquals 比 (object)o1 == (object)o2 更能揭示意图。它在代码中清楚地表明“我正在测试引用相等/身份”,而不是将意图隐藏在一堆强制转换下。

    【讨论】:

    • 我同意强制转换可能会产生误导,但您不认为 '==' 运算符被普遍认为是参考比较吗?
    • 不,相反。例如,string1 == string2 执行值比较而不是引用比较。因此,原始海报需要强制转换为对象以强制进行参考比较。
    • 再说一遍你的第一句话:我相信正好相反(至少在Rotor中是这样实现的),但这并不重要。
    • 我也相信第一句话是相反的。 ReferenceEquals 的反射 c# 源实现为 object == object。这就是导致我使用(对象)转换来实现引用相等的原因,因为我认为它可能会绕过函数调用并且更快。
    • 感谢 Konrad 和 Triynko,你是对的。我已编辑帖子以更正错误。
    【解决方案4】:

    加上我的两分钱,在性能关键代码的许多后期,在非常大的代码库上,有时疯狂的深度调用深度。

    在现实世界中的“微基准测试”之外,JIT 有更多的问题和顾虑,既没有 C++ WPO 编译时的奢华,也没有 C# 编译器更直接的翻译,但所有在 C# 编译器完成后没有必要拥有所有上下文的问题。

    “迂腐”的形式:

    if ((object)a == (object)b) { }     // ref equals
    
    if (!((object)a == (object)b)) { }  // ref not equals
    

    如果您确实需要权衡和衡量真实的性能问题,或者需要为一些非常普遍的大型课程减轻 JIT 的压力,这可以提供很大帮助。 NullOrEmpty vs '(object)str == null || 也是如此str.Length == 0'。

    没有 WPO 的奢侈,并且在许多情况下不知道哪些程序集在 JITing 受到重击后可能会被加载或卸载,奇怪的非确定性事情会发生关于优化什么以及如何优化.

    这是一个很大的话题,但这里有几点:

    1. 到目前为止,JIT 将追逐内联并注册优化向下调用深度,并且完全取决于当时发生的其他情况。如果您由于使用而最终在链上编译一次函数,而在更远的链上编译一次不同的运行,您可以获得截然不同的结果。对于许多受延迟或 UI 驱动的应用程序来说,最糟糕的事情是非确定性,而在较大的应用程序中,这可能会迅速增加。

    2. !((object)a == (object)b) 和 (object)a != (object)b 并不总是编译成相同的代码,这是真的当然对于 !(a == b) 和 a != b,即使没有任何显式运算符或 Equals 覆盖。 '(object)a != (object)b' 更有可能触发 .Net 自己对运行时更迂腐的调用,这非常昂贵。

    3. 如果对 JIT 非常有帮助,请尽早使用 '(object)' 或 'RefEquals' 进行保护,即使您目前没有 operator 或 Equals 覆盖。 (对象)甚至更好。如果您考虑 JIT 必须做些什么来确定一个类型是否可以覆盖,并处理 bizantine (sp) Equality 规则等等,它就像一个迷你地狱,以及任何可以稍后公开的东西,你打算参考平等,您可以避免以后突然减速或不稳定的代码。如果它已经是公开的并且没有被密封,JIT 就无法确定规则将会或有时间追捕它们。

    4. 保护通常更普遍的“空”检查可能更重要,尽管不是 OP 问题的一部分,因为通常适用相同的规则和问题。 '(object)a == null' 和 '!((object)a == null)' 是 'pedantic' 等价物。

    【讨论】:

    • +1 你的观点很有趣。你有没有在另一个问题或博客文章中详细写过这个?我想更多地了解您是如何得出这些结论的。
    【解决方案5】:

    前面提到的article about == operator 更好地提供了不完整的信息,至少在 .NET 4.0 上是这样(好吧,它被写回了 2.0 倍)。

    它指出 ReferenceEquals 没有被优化/内联,只有当您使用“AnyCPU”配置构建项目时才会如此。设置为 'x86' 或 'x64' 会使 (object)obj == null 和 ReferenceEquals(object, null) 最终成为相同的 IL,其中两种方法都只是一个 'ceq' IL 指令。

    所以答案是:两种方法生成的 IL 在 Release / x86 或 x64 版本上是相同的。

    ReferenceEquals 绝对更具可读性,至少在我看来。

    【讨论】:

    • 虽然它归结为可以忽略不计的差异,但无论如何还是有一些差异..请参阅我在 x86 平台上的答案..
    【解决方案6】:
    public static bool ReferenceEquals (Object objA, Object objB) {
            return objA == objB;
        }
    

    http://referencesource.microsoft.com/#mscorlib/system/object.cs,4d607d6d56a93c7e

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-09-26
      • 1970-01-01
      • 2018-04-27
      • 2012-07-30
      • 2019-09-05
      • 2016-12-13
      • 1970-01-01
      相关资源
      最近更新 更多