Will these methods bypass expensive null checks?

I know that using null checks can be expensive. The main reason I found out about this is that I use JetBrains Rider IDE and it marks (gameObject == null) as expensive when placed in an Update method. While it does this, it doesn’t seem to mark these two alternatives to null checks as expensive. I would like to know if the below null checks are actually not expensive or just an oversight from my IDE?

if( gameObject == null ){}  // Marked as an expensive null check

if( gameObject ){}  // Not marked as an expensive null check
if( ReferenceEquals( gameObject, null ) ){}  // Not marked as an expensive null check

EDIT: According to the end of this article using if( gameObject ){} might actually be more efficient.

Your IDE is making you paranoid. I’m pretty sure your first two examples compile down to the exact same IL code. Don’t hurt yourself with premature optimizations. If you’re seeing a performance issue in your game, then it’s time to buckle down with the profiler and see what the problem is.

4 Likes

Not paranoid, but rather I like to improve my code practice as much a possible. I’d rather save up that extra performance as I’m working and not leave it for last. Also, Unity wrote a blog about this. Here’s an excerpt.

Turns out two of our own engineers missed that the null check was more expensive than expected, and was the cause of not seeing any speed benefit from the caching. This led to the “well if even we missed it, how many of our users will miss it?”, which results in this blogpost :slight_smile:

Despite that blog post, I think this still applies:

1 Like

When in doubt, do a performance test. Should be pretty simple to benchmark all 3. Make sure you test in a build, not the editor. Probably would take about 20 minutes to write the test and execute it.

The first 2… == null, and implicit bool cast, are going to be REALLY similar in performance.

Case in point… here is the source code causing your concerns:

        // Does the object exist?
        public static implicit operator bool(Object exists)
        {
            return !CompareBaseObjects(exists, null);
        }

        static bool CompareBaseObjects(UnityEngine.Object lhs, UnityEngine.Object rhs)
        {
            bool lhsNull = ((object)lhs) == null;
            bool rhsNull = ((object)rhs) == null;

            if (rhsNull && lhsNull) return true;

            if (rhsNull) return !IsNativeObjectAlive(lhs);
            if (lhsNull) return !IsNativeObjectAlive(rhs);

            return lhs.m_InstanceID == rhs.m_InstanceID;
        }
    
        public static bool operator==(Object x, Object y) { return CompareBaseObjects(x, y); }

        public static bool operator!=(Object x, Object y) { return !CompareBaseObjects(x, y); }

Literally the == and the implicit bool cast call the same ‘CompareBaseObjects(obj, null)’. They follow the SAME logic.

Any speed difference you might get running thousands of loop tests on it really just shows the margin of error in doing tests like that.

Also, that article doesn’t really suggest that it’s more efficient. Really it says:

They’re referring to the fact that “if (obj != null)” is a valid command for any System.Object, but the implicit bool cast of “if (obj)” only works if the implicit conversion exists, which it doesn’t for System.Object, and thusly the compiler will yell at you. Which avoids falling into the trap of accidentally using “if (obj != null)” when obj is cast to say System.Object, or an interface, or whatever (a trap many, including myself, have fallen into… and is an argument against this trash operator that Unity debated removing but didn’t because it’d break legacy code/tutorials/documentation).

Also, the 3rd line of code you show… “ReferenceEquals(obj, null)” doesn’t even behave the same way as the first 2. And is the whole argument about the != null thing!

Replacing != null or implicit bool conversion with it because it is every so slightly faster, isn’t necessarily good. Main reason being… it’s faster because it doesn’t do the same thing! That code just checks if it’s actually null. Where as the first 2 check if it’s actually null OR is destroyed.

I want to repeat this… the 3rd option DOES NOT BEHAVE THE SAME as the first 2 options.

And when Unity talks about == being expensive. They’re referring to the fact that it doesn’t behave the same as a standard null check (ReferenceEquals, or == when cast as something not UnityEngine.Object or subtype thereof). The engineers who wrote the == overload didn’t account for how much more expensive it is due to the multiple stacks allocations, multiple standard null checks/branches, and the call into native code. Cause of course all of those extra lines of code are more expensive. It’s literally like 10 extra operations, including 2 native interop calls.

Lastly, as @PraetorBlue has been saying… these optimization are TINY. EENSY WEENSY TINY! Write code that makes sense to you, then optimize when the profiler tells you otherwise.

4 Likes

Thanks for the reply!

I’m very late to the party, but the Rider IDE also dropped me into a rabbit hole.
First of all, I did some super basic benchmarking in the editor:

Stopwatch stopwatch = new Stopwatch();
for (int i = 0; i < n / 10; i++)//warmup
{
    action.Invoke();
}

stopwatch.Start();
for (int i = 0; i < n; i++) action.Invoke();
stopwatch.Stop();

Debug.Log(prefix + stopwatch.Elapsed);

10’000’000 times each:
(ss:ms)
if (go == null) 00.0841345
if (go) 00.0805793
if (go is null) 00.0178943
if (ReferenceEquals(go, null)) 00.0179931

Those aren’t averages, but go == null & (go) are basically the same (standard deviation).
Same case with go is null & ReferenceEquals (I use go is null, because it looks better).

I did change most of my “go == null” to “go is null”, which caused some problems here and there. Because like others mentioned, it works completely different. Granted, it is 4± times faster on average. But with 10 million iterations it’s not even 0.1s. So with hindsight 20/20, go == null shouldn’t really be a “performance expensive”. There might be some more complexity if the object is getting destroyed & referenced over and over.

So I “partially” checked for that, and destroyed the object after (go == null), and added another (go==null) after the destruction.

Not destroyed:
if (go == null) 00.0838304

Destroyed:
if (go == null) 01.0204344
if (go) 01.0133544
if (go is null) 00.0180781
if (ReferenceEquals(go, null)) 00.0179518

Safe to say, don’t usually use go is null/refenceEquals, unless you want that specific functionality. OR you have profiled and somehow checking go == null every frame is bad for your performance (do it instead every few frames with Time.frameCount % 10, or in a coroutine, or with a timer of some kind).

7 Likes