40

Referring to the following SE answer.

When writing

A = A ?? B;

it is the same as

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

Does that mean that

if( null == A ) A = B;

would be preferred, performance wise?

Or can I assume that the compiler optimizes the code when the same object is in the ?? notation?

Community
  • 1
  • 1
Lockszmith
  • 2,173
  • 1
  • 29
  • 43
  • Yea, the performance hit here is non existent. – eandersson Jul 10 '12 at 11:49
  • 3
    Write both versions, compile, use ILDasm or dotPeek to see if there are any difference in generated code. – J0HN Jul 10 '12 at 11:50
  • 2
    Minus marks readability. Checking if null is equal to anything doesn't really make sense. Maybe just preference but I always use if (A != null). You are performing a check on A, not null. – Talon Sep 09 '16 at 07:35
  • 2
    The order of a condition is mostly a preference. I find it more readable to put the value of comparison on the left, as it puts the focus on the end result. The habit is an old one, and stems from coding in C and C++ where '=' could accidentally assign while comparing, in C# it's no longer relevant, yet I find the comparison more readable. If you have any interest in C/C++ check out the still relevant Effective C++ book (http://rads.stackoverflow.com/amzn/click/0321334876), also see the following discussion: http://stackoverflow.com/questions/6883646/obj-null-vs-null-obj – Lockszmith Sep 09 '16 at 21:49

5 Answers5

22

Don't worry about the performance, it will be negligible.

If you are curious about it, write some code to test the performance using Stopwatch and see. I suspect you'll need to do a few million iterations to start seeing differences though.

You can also never assume about the implementation of things, they are liable to change in future - invalidating your assumptions.

My assumption is the performance difference is likely very, very small. I'd go for the null coalescing operator for readability personally, it is nice and condense and conveys the point well enough. I sometimes do it for lazy-load checking:

_lazyItem = _lazyItem ?? new LazyItem();
Adam Houldsworth
  • 63,413
  • 11
  • 150
  • 187
  • Adam, due to the new answer, I'm changing my stance, while readability is key, the side-effect should not be overlooked. I'd rather promote good practice, and so I'm switching to VCD's answer as the accepted one. – Lockszmith Jul 16 '17 at 17:10
  • @Lockszmith I was just about to try VCD's code when I noticed the contrived setter. It does raise a good point, I'll give you that, but performance profiling would highlight this. Bad practice in the form of poor property setters shouldn't dictate a usage style over `??`. The side-effect isn't over the coalescing operator, it is because the operator in your example is used in conjunction with assignment. – Adam Houldsworth Jul 17 '17 at 06:58
  • you are right - I read the code again, and his answer has nothing to do with the ?? operator, but with the value this is assigning to. I'll pass the accepted answer back here. Good catch. (note to self: read code more thoroughly next time) – Lockszmith Jul 20 '17 at 14:57
  • As of C# 8, you can now use null coalesce assignment. Using the lazy-load check example, it can be declared simply as `_lazyItem ??= new LazyItem();` – Myles Apr 09 '21 at 10:40
4

Although performance for ?? is negligible, the side effect sometimes may not be negligible. Consider the following program.

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();
        }

    }
}

Run result on my PC.

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

Because there is an extra assignment using ??, and the performance of the assignment sometimes might not guaranteed. This might lead to a performance issue.

I would rather use if( null == A ) A = B; instead of A = A ?? B;.

VCD
  • 889
  • 10
  • 27
  • @VCD Technically there are no side-effects here, you are explicitly requesting an assignment. It does raise an interesting point, granted, but not one that is specific to the Null Coalescing operator. – Adam Houldsworth Jul 17 '17 at 07:00
  • Still @VCD raised a valid point to look out when using (any) assignment. – Lockszmith Jul 20 '17 at 15:01
  • Maybe the term I use wasn't accurate enough, rather than `side-effect not negligible` maybe I should use the world `end result is unexpected`. – VCD Jul 31 '17 at 06:49
3

My advice would be to inspect the IL (intermediate language) and compare the different results. You can then see exactly what each boils down to and decide what is more optimized. But as Adam said in his comment, you're most likely best to focus on readability/maintainability over performance in something so small.

EDIT: you can view the IL by using the ILDASM.exe that ships with visual studio and open your compiled assembly.

OnResolve
  • 4,016
  • 3
  • 28
  • 50
  • More useful would be to look at the machine code / asm after JIT optimization; a significant amount of optimization happens in that step. Use https://sharplab.io/ for example. – Peter Cordes Mar 25 '22 at 01:01
3

I just tried this in C# - very quickly, so there could be an error in my method. I used the following code and determined that the second method took about 1.75 times longer than the first.
@Lockszmith: After the edit below, the ratio was 1.115 in favor of the 1st implementation

Even if they took the same time, I would personally use the language construct that is built in, as it expresses your intentions more clearly to any future compiler that may have more built-in optimizations.

@Lockszmith: I've edited the code to reflect the recommendations from the comments

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");
Lockszmith
  • 2,173
  • 1
  • 29
  • 43
Stephen Hewlett
  • 2,415
  • 1
  • 18
  • 31
  • 1
    I've just realised that this means the built in language construct takes longer. This is surprising... maybe there is some other optimisation being performed on the code that makes the test unfair? – Stephen Hewlett Jul 10 '12 at 12:07
  • 1
    Not really, the ?? route does a null check and assignment every time whereas the `A == null` route simply does the null check and no assignment. Try a test where `A` needs assigning *every* time. Also, use `Stopwatch` as opposed to `DateTime`, it is more accurate. Plus you need to do at least one run to make sure the lot has been JIT'd before doing a timing run. – Adam Houldsworth Jul 10 '12 at 12:17
  • Thank for the code, I've edited your answer to reflect the suggestions above, and run it in LINQpad, it's still favoring the 'long version', but as all mentioned, what counts here is readability - and your test proves it's negligible. – Lockszmith Jul 10 '12 at 12:40
3

Yes, there is a difference.

Using Visual Studio 2017 15.9.8 targeting .NET Framework 4.6.1. Consider the sample below.

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}");
}

Using ILDasm it becomes clear that the compiler does not treat those statements equally.

?? operator:

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

Conditional null check:

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

Apparently, the ?? operator implies a duplication of the stack value (should be the s variable right?). I ran a simple test (multiple times) to get a feeling of which of the two is faster. Operating on string, running on this particular machine, I got these average numbers:

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

Benchmark sample code:

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");
}

So the answer is yes, there is a difference.

UPDATE

Analyzing the IL instructions on sharplab.io we can see that ?? operator produces a dup instruction whereas the ternary operator does not. Benchmarks indicate that the ?? operator produces faster code.

I believe the optimizer/jitter is able to perform some tricks when it knows there are no side effects. I.e. a duplicated value from dup will not change through its lifetime whereas the value from ldarg.1 could change, e.g. from another thread.

PS. StackOverflow should auto-warn posts mentioning "performance" and "negligible" in the same sentence. Only the original poster can know for sure if a time unit is neglible.

l33t
  • 18,692
  • 16
  • 103
  • 180
  • Thanks for the detailed analysis, I agree with it completely, but I think the reason for not using the operator in this case would be because of what [VCD describes in their answer](https://stackoverflow.com/a/45073886/799379). So I marked his answer as the accepted one. – Lockszmith May 22 '19 at 14:33
  • Yes, his answer is definitely relevant. It's certainly interesting that `??` seems faster than a traditional `if..else`. – l33t May 24 '19 at 07:16
  • More useful would be to look at the machine code / asm after JIT optimization; a significant amount of optimization happens in that step. Use https://sharplab.io/ for example. – Peter Cordes Mar 25 '22 at 01:02
  • @PeterCordes Updated the answer with a `sharplab.io` sample and some additional notes. Haven't analyzed the `jit/asm` though, as the site crashed when doing so. However, I think the nature of the `IL` instructions makes it clear why `??` is faster. – l33t Mar 25 '22 at 03:39
  • For local vars that live in registers, there's no reason a good optimizer can't optimize away the extra logic of the self-assignment even if it's present in the IL. C#'s JIT optimizer might not take the time to optimize well, but the fact that it takes some extra stack operations in the IL doesn't guarantee worse asm. Sharplab seems to be down for me, too, (probably just timing coincidence; it's worked for me in the past showing asm). – Peter Cordes Mar 25 '22 at 04:12
  • https://godbolt.org/z/WYfcaGKbY shows the clang compiling similar C++ optimizes to good asm either way; I was hoping to show it optimizing more complicated LLVM-IR from one source down into the same machine code, but it turns out it does this optimization in the clang front-end. I still expect it could optimize away a self-assignment if there was one in the LLVM-IR. (C++ `char*` is very likely simpler than a C# string, so this task is pretty trivial for the example I picked.) – Peter Cordes Mar 25 '22 at 04:12