【问题标题】:Double question marks ('??') vs if when assigning same var双问号 ('??') vs if 分配相同的 var
【发布时间】:2012-07-09 21:21:07
【问题描述】:

参考以下SE answer

写作时

A = A ?? B;

和这个一样

if( null != A )
    A = A;
else
    A = B;

这是否意味着

if( null == A ) A = B;

在性能方面会更受欢迎吗?

或者我可以假设当同一个对象在?? 表示法中时编译器会优化代码?

【问题讨论】:

  • 是的,这里的性能命中是不存在的。
  • 两个版本都写,编译,用ILDasm或者dotPeek看看生成的代码有没有区别。
  • 减号标记可读性。检查 null 是否等于任何东西并没有真正的意义。也许只是偏好,但我总是使用 if (A != null)。您正在对 A 进行检查,而不是 null。
  • 条件的顺序主要是一种偏好。我发现将比较的值放在左侧更具可读性,因为它将重点放在最终结果上。这个习惯是一个古老的习惯,源于 C 和 C++ 中的编码,其中 '=' 在比较时可能会意外分配,在 C# 中它不再相关,但我发现比较更具可读性。如果您对 C/C++ 感兴趣,请查看仍然相关的 Effective C++ 书籍 (rads.stackoverflow.com/amzn/click/0321334876),另请参阅以下讨论:stackoverflow.com/questions/6883646/obj-null-vs-null-obj

标签: c# compiler-optimization null-coalescing-operator


【解决方案1】:

不用担心性能,它可以忽略不计。

如果您对此感到好奇,请编写一些代码以使用Stopwatch 测试性能并查看。我怀疑您需要进行几百万次迭代才能开始看到差异。

您也永远不能假设事物的实现,它们可能会在未来发生变化 - 使您的假设无效。

我的假设是性能差异可能非常非常小。我个人会选择 null 合并运算符以提高可读性,它很好而且很简洁,并且很好地传达了这一点。我有时会这样做以进行延迟加载检查:

_lazyItem = _lazyItem ?? new LazyItem();

【讨论】:

  • 亚当,由于新的答案,我正在改变立场,虽然可读性是关键,但不应忽视副作用。我宁愿提倡良好的做法,所以我改用 VCD 的答案作为公认的答案。
  • @Lockszmith 我正要尝试 VCD 的代码时,我注意到了做作的二传手。它确实提出了一个很好的观点,我会给你,但性能分析会突出这一点。糟糕的属性设置器形式的不良做法不应该超过?? 的使用风格。副作用不是合并运算符,而是因为您的示例中的运算符与赋值结合使用。
  • 你说得对——我又读了一遍代码,他的回答与 ??运算符,但使用 this 分配的值。我将把接受的答案传回这里。接得好。 (自我提醒:下次更彻底地阅读代码)
  • 从 C# 8 开始,您现在可以使用空合并赋值。使用延迟加载检查示例,它可以简单地声明为_lazyItem ??= new LazyItem();
【解决方案2】:

我的建议是检查 IL(中间语言)并比较不同的结果。然后,您可以准确地看到每个归结为什么,并决定什么是更优化的。但正如亚当在他的评论中所说,您最有可能最好关注可读性/可维护性,而不是这么小的东西的性能。

编辑:您可以使用 Visual Studio 附带的 ILDASM.exe 查看 IL 并打开已编译的程序集。

【讨论】:

    【解决方案3】:

    我刚刚在 C# 中尝试过 - 非常快,所以我的方法可能有错误。我使用以下代码并确定第二种方法比第一种方法花费了大约 1.75 倍。
    @Lockszmith:在下面的编辑之后,比率为 1.115,有利于第一种实现 p>

    即使他们花费相同的时间,我个人也会使用内置的语言结构,因为它更清楚地向任何可能具有更多内置优化的未来编译器表达了您的意图。

    @Lockszmith:我已经编辑了代码以反映 cmets 的建议

    var A = new object();
    var B = new object();
    
    var iterations = 1000000000;
    
    var sw = new Stopwatch();
    for (int i = 0; i < iterations; i++)
    {   
        if( i == 1 ) sw.Start();
        if (A == null)
        {
            A = B;
        }
    }
    sw.Stop();
    var first = sw.Elapsed;
    
    sw.Reset();
    for (int i = 0; i < iterations; i++)
    {
        if( i == 1 ) sw.Start();
        A = A ?? B;
    }
    sw.Stop();
    var second = sw.Elapsed;
    
    first.Dump();
    second.Dump();
    
    (first.TotalMilliseconds / second.TotalMilliseconds).Dump("Ratio");
    

    【讨论】:

    • 我刚刚意识到这意味着内置语言结构需要更长的时间。这很令人惊讶……也许对代码进行了一些其他优化,导致测试不公平?
    • 不是真的,??路由每次都会进行空值检查和赋值,而 A == null 路由只是进行空值检查而不进行赋值。尝试测试A 需要每次 分配。另外,使用Stopwatch 而不是DateTime,它更准确。另外,在计时运行之前,您需要至少运行一次以确保批次已经过 JIT。
    • 感谢代码,我已经编辑了您的答案以反映上述建议,并在 LINQpad 中运行它,它仍然支持“长版本”,但正如所有提到的,这里重要的是可读性- 你的测试证明它可以忽略不计。
    【解决方案4】:

    虽然?? 的性能可以忽略不计,但副作用有时可能不可忽略。考虑以下程序。

    using System;
    using System.Diagnostics;
    using System.Threading;
    
    namespace TestProject
    {
        class Program
        {
            private string str = "xxxxxxxxxxxxxxx";
            public string Str
            {
                get
                {
                    return str;
                }
                set
                {
                    if (str != value)
                    {
                        str = value;
                    }
                    // Do some work which take 1 second
                    Thread.Sleep(1000);
                }
            }
    
            static void Main(string[] args)
            {
                var p = new Program();
    
                var iterations = 10;
    
                var sw = new Stopwatch();
                for (int i = 0; i < iterations; i++)
                {
                    if (i == 1) sw.Start();
                    if (p.Str == null)
                    {
                        p.Str = "yyyy";
                    }
                }
                sw.Stop();
                var first = sw.Elapsed;
    
                sw.Reset();
                for (int i = 0; i < iterations; i++)
                {
                    if (i == 1) sw.Start();
                    p.Str = p.Str ?? "yyyy";
                }
                sw.Stop();
                var second = sw.Elapsed;
    
                Console.WriteLine(first);
                Console.WriteLine(second);
    
                Console.Write("Ratio: ");
                Console.WriteLine(second.TotalMilliseconds / first.TotalMilliseconds);
                Console.ReadLine();
            }
    
        }
    }
    

    在我的电脑上运行结果。

    00:00:00.0000015
    00:00:08.9995480
    Ratio: 5999698.66666667
    

    因为有一个使用?? 的额外分配,并且有时可能无法保证分配的性能。这可能会导致性能问题。

    我宁愿使用if( null == A ) A = B; 而不是A = A ?? B;

    【讨论】:

    • @VCD 从技术上讲,这里没有副作用,您明确要求分配。它确实提出了一个有趣的观点,这是理所当然的,但不是 Null Coalescing 运算符特有的。
    • 在使用(任何)分配时,@VCD 仍然提出了一个需要注意的有效点。
    • 也许我用的术语不够准确,而不是side-effect not negligible,也许我应该使用世界end result is unexpected
    【解决方案5】:

    是的,有区别。

    使用Visual Studio 2017 15.9.8 定位.NET Framework 4.6.1。考虑下面的示例。

    static void Main(string[] args)
    {
        // Make sure our null value is not optimized away by the compiler!
        var s = args.Length > 100 ? args[100] : null;
        var foo = string.Empty;
        var bar = string.Empty;
    
        foo = s ?? "foo";
        bar = s != null ? s : "baz";
    
        // Do not optimize away our stuff above!
        Console.WriteLine($"{foo} {bar}");
    }
    

    使用ILDasm 很明显,编译器不会平等对待这些语句。

    ??运营商:

    IL_001c:  dup
    IL_001d:  brtrue.s   IL_0025
    IL_001f:  pop
    IL_0020:  ldstr      "foo"
    IL_0025:  ldloc.0
    

    条件空检查

    IL_0026:  brtrue.s   IL_002f
    IL_0028:  ldstr      "baz"
    IL_002d:  br.s       IL_0030
    IL_002f:  ldloc.0
    

    显然,?? 运算符意味着堆栈值的重复(应该是 s 变量吧?)。我运行了一个简单的测试(多次)来获得感觉两者中哪一个更快。在string 上运行,在这台特定的机器上运行,我得到了这些平均数字:

    ?? operator took:           583 ms
    null-check condition took: 1045 ms
    

    基准示例代码:

    static void Main(string[] args)
    {
        const int loopCount = 1000000000;
        var s = args.Length > 1 ? args[1] : null; // Compiler knows 's' can be null
        int sum = 0;
    
        var watch = new System.Diagnostics.Stopwatch();
        watch.Start();
    
        for (int i = 0; i < loopCount; i++)
        {
            sum += (s ?? "o").Length;
        }
    
        watch.Stop();
    
        Console.WriteLine($"?? operator took {watch.ElapsedMilliseconds} ms");
    
        sum = 0;
    
        watch.Restart();
    
        for (int i = 0; i < loopCount; i++)
        {
            sum += (s != null ? s : "o").Length;
        }
    
        watch.Stop();
    
        Console.WriteLine($"null-check condition took {watch.ElapsedMilliseconds} ms");
    }
    

    所以答案是肯定的,是有区别的。

    PS。 StackOverflow 应该自动警告在同一个句子中提到“性能”和“可忽略不计”的帖子。只有原始发布者才能确定时间单位是否可以忽略。

    【讨论】:

    • 感谢您的详细分析,我完全同意,但我认为在这种情况下不使用运算符的原因可能是因为VCD describes in their answer。所以我将他的答案标记为已接受。
    • 是的,他的回答绝对是相关的。 ?? 似乎比传统的 if..else 更快,这当然很有趣。
    猜你喜欢
    • 2011-09-26
    • 1970-01-01
    • 2013-07-12
    • 2019-05-02
    • 2014-05-31
    • 1970-01-01
    • 2011-02-15
    • 2014-09-28
    • 2011-01-23
    相关资源
    最近更新 更多