Performance Surprise with "As" and Nullable Types

Performance surprise with as and nullable types

Clearly the machine code the JIT compiler can generate for the first case is much more efficient. One rule that really helps there is that an object can only be unboxed to a variable that has the same type as the boxed value. That allows the JIT compiler to generate very efficient code, no value conversions have to be considered.

The is operator test is easy, just check if the object isn't null and is of the expected type, takes but a few machine code instructions. The cast is also easy, the JIT compiler knows the location of the value bits in the object and uses them directly. No copying or conversion occurs, all machine code is inline and takes but about a dozen instructions. This needed to be really efficient back in .NET 1.0 when boxing was common.

Casting to int? takes a lot more work. The value representation of the boxed integer is not compatible with the memory layout of Nullable<int>. A conversion is required and the code is tricky due to possible boxed enum types. The JIT compiler generates a call to a CLR helper function named JIT_Unbox_Nullable to get the job done. This is a general purpose function for any value type, lots of code there to check types. And the value is copied. Hard to estimate the cost since this code is locked up inside mscorwks.dll, but hundreds of machine code instructions is likely.

The Linq OfType() extension method also uses the is operator and the cast. This is however a cast to a generic type. The JIT compiler generates a call to a helper function, JIT_Unbox() that can perform a cast to an arbitrary value type. I don't have a great explanation why it is as slow as the cast to Nullable<int>, given that less work ought to be necessary. I suspect that ngen.exe might cause trouble here.

Nullable types boxing in performance recommendations


Is this correct?

No, it's not correct. int? is the same as Nullable<int>, which is still a value type, and which is still returned from the method without a heap allocation, and without boxing.

And it seems that Microsoft has noticed this erroneous passage in the documentation, because it's been removed. Here's a cached version of the page, with the passage included.

I assume this change was recent, because I further assume that when you posted the link to the documentation page, it actually did include that passage, hence the question itself. But the page definitely does not include that passage now. Indeed, there was an issue opened on Github concerning this exact passage, and that issue was closed (so, presumably fixed) two hours ago (midday on November 7, 2020, PST).

That said, please note: the stack is an implementation detail. Using value types can (as in this case) avoid the need to allocate on the heap, but value types can and do live on the heap in other scenarios, and so if one is concerned about heap vs stack allocations, one needs to base one's predictions on something other than strictly the question of value vs reference type.

Is there a performance degradation when we ALWAYS use nullable value types instead of value types?

As Mitch Wheat pointed about above, no, you should not worry about this. I'm going to give you the short answer reason now, and later I'm going to help you discover more about what you're asking:

Write your code to be correct. Profile after writing so that you find the points that are causing you grief.

When you have code that constantly uses Nullable and you have performance reasons and you profile it and you can't find the problem yourself, then come ask us how to make it faster. But no, the overhead of using Nullable is for all intents and purposes not degrading.

Discover more about what you're asking:

  • Performance surprise with "as" and nullable types
  • Why shouldn't I always use nullable types in C#
  • C# Performance gain returning a Nullable Type from a SqlDataReader
  • Alternatives to nullable types in C#
  • Casting (int?)null vs. new int?() - Which is better?
  • Boxing / Unboxing Nullable Types - Why this implementation?

Now, having read all those pages, I hope you feel more enlightened.

What's the theoretically fastest access to member of nullable type in C#?

Just for curiosity:

public struct MyType
{
public int SomeMember { get; set; }
}

Some very raw tests, disabling compiler optimizations:

MyType? thisCantBeNull = new MyType();
MyType someDefaultValue = new MyType();

var t1 = new Action(() => { var r = (thisCantBeNull ?? someDefaultValue).SomeMember; });
var t2 = new Action(() => { var r = ((MyType)thisCantBeNull).SomeMember; });
var t3 = new Action(() => { var r = thisCantBeNull.Value.SomeMember; });
var t4 = new Action(() => { var r = thisCantBeNull.GetValueOrDefault().SomeMember; });

const int times = 1000 * 1000 * 1000;

var r1 = t1.RunNTimes(times);
// Elapsed Timespan = 00:00:14.45115

var r2 = t2.RunNTimes(times);
// Elapsed Timespan = 00:00:07.9415388

var r3 = t3.RunNTimes(times);
// Elapsed Timespan = 00:00:08.0096765

var r4 = t4.RunNTimes(times);
// Elapsed Timespan = 00:00:07.4732878

The same tests, enabling compiler optimizations:

var r1 = t1.RunNTimes(times);
// Elapsed Timespan = 00:00:02.9142143

var r2 = t2.RunNTimes(times);
// Elapsed Timespan = 00:00:02.4417182

var r3 = t3.RunNTimes(times);
// Elapsed Timespan = 00:00:02.6278304

var r4 = t4.RunNTimes(times);
// Elapsed Timespan = 00:00:02.1725020

Where RunNTimes is:

public static TimeSpan RunNTimes(this Action a, int nTimes = 1)
{
if (nTimes == 0)
throw new ArgumentException("0 times not allowed", nameof(nTimes));

var stopwatch = new Stopwatch();

stopwatch.Start();
for (int i = 0; i < nTimes; ++i)
a();
stopwatch.Stop();

return stopwatch.Elapsed;;
}

Explicit nullable types and where != null

The compiler is not "smart" enough to detect that the argument to Select() cannot be null in this case, as it only does static code analysis. For situations like these, the ! notation was introduced with nullable reference types. Applying an exclamation mark to an object tells the compiler to "shut up" about nullability warnings.

var result = aFunction()
.Where(data => data != null)
.Select(data => data!.Id).ToList();

This means that the compiler will not generate a warning that data could be null. This is helpful in all cases where you know that the value is not null, but the compiler (e.g. due to the complexity of the code) doesn't properly detect that.

Note that this really only removes the warning. If the value is, in fact, null, the code will behave as previously, so it would throw a NullReferenceException.

C# Performance gain returning a Nullable Type from a SqlDataReader

I seem to recall that it can sometimes by faster to get the value as an object and then check if that's DBNull.

private Int32? GetNullableInt32(SqlDataReader dataReader, int fieldIndex)
{
object value = dataReader.GetValue(fieldIndex);
return value is DBNull ? (Int32?) null : (Int32) value;
}

It's at least worth a try. Note that this is assuming you can unbox straight to an int... I don't know for sure whether that's correct, but it'll be easy to see.

Now there's another approach which is somewhat less safe - it will return null for any non-integer value, even if the field is actually a string, for example:

private Int32? GetNullableInt32(SqlDataReader dataReader, int fieldIndex)
{
return dataReader.GetValue(fieldIndex) as Int32?;
}

I've previously written about "as" with nullable types not being as fast as I'd expect, but this may be a slightly different case... again, it's worth having a go.

However, I'd be really surprised if this is genuinely a bottleneck... surely getting the data from the database in the first place is going to be far more expensive. Do you have benchmarks for this?

nullable context doesn't work properly in c#

The C# language does not guarantee that the property stepStatus returns the same value when the getter is called multiple times. Therefore null-checking it once is no proof that it will not be null the second time.



Related Topics



Leave a reply



Submit