I came across this while doing some benchmarking.
bool b;
MyStruct s;
for (int i = 0; i < 10000000; i++)
{
b = (object)s == null;
}
Debug: 200 ms
Release: 5 ms
bool b;
MyStruct? s = null;
for (int i = 0; i < 10000000; i++)
{
b = (object)s == null;
}
Debug: 800 ms
Release: 800 ms
I can understand this result since casting the nullable struct to object
gives me a boxed type of that struct. But why isn't casting struct s
to object
for doing null comparison (as in the first method) result in the same performance? Is it that compiler is optimizing the call to return false
always as a struct can't be null?
You can't. Struct are considered value types, and by definition can't be null. The easiest way to make it nullable is to make it a reference type.
For variables of class types and other reference types, this default value is null . However, since structs are value types that cannot be null , the default value of a struct is the value produced by setting all value type fields to their default value and all reference type fields to null .
Boxing a value of a nullable-type produces a null reference if it is the null value (HasValue is false), or the result of unwrapping and boxing the underlying value otherwise. will output the string “Box contains an int” on the console.
Yes, the compiler is optimising it.
It knows that a struct can never be null, so the result of casting it to an object can never be null - so it will just set b
to false in the first sample. In fact, if you use Resharper, it will warn you that the expression is always false.
For the second of course, a nullable can be null so it has to do the check.
(You can also use Reflector
to inspect the compiler-generated IL code to verify this.)
The original test code is not good because the compiler knows that the nullable struct will always be null and will therefore also optimize away that loop. Not only that, but in a release build the compiler realises that b
is not used and optimizes away the entire loop.
To prevent that, and to show what would happen in more realistic code, test it like so:
using System;
using System.Diagnostics;
namespace ConsoleApplication1
{
internal class Program
{
private static void Main(string[] args)
{
bool b = true;
MyStruct? s1 = getNullableStruct();
Stopwatch sw = Stopwatch.StartNew();
for (int i = 0; i < 10000000; i++)
{
b &= (object)s1 == null; // Note: Redundant cast to object.
}
Console.WriteLine(sw.Elapsed);
MyStruct s2 = getStruct();
sw.Restart();
for (int i = 0; i < 10000000; i++)
{
b &= (object)s2 == null;
}
Console.WriteLine(sw.Elapsed);
}
private static MyStruct? getNullableStruct()
{
return null;
}
private static MyStruct getStruct()
{
return new MyStruct();
}
}
public struct MyStruct {}
}
in fact both loop will have an empty body when compiled!
to make the second loop behave, you will have to remove the (object)
casting
this is what it look like when I compile your code,
public struct MyStruct
{
}
class Program
{
static void Main(string[] args)
{
test1();
test2();
}
public static void test1()
{
Stopwatch sw = new Stopwatch();
bool b;
MyStruct s;
for (int i = 0; i < 100000000; i++)
{
b = (object)s == null;
}
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);
Console.ReadLine();
}
public static void test2()
{
Stopwatch sw = new Stopwatch();
bool b;
MyStruct? s = null;
for (int i = 0; i < 100000000; i++)
{
b = (object)s == null;
}
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);
Console.ReadLine();
}
}
IL:
the MyStruct (empty since you didn't provide any)
.class public sequential ansi sealed beforefieldinit ConsoleApplication1.MyStruct
extends [mscorlib]System.ValueType
{
.pack 0
.size 1
} // end of class ConsoleApplication1.MyStruct
the first loop in your example
.method public hidebysig static
void test1 () cil managed
{
// Method begins at RVA 0x2054
// Code size 17 (0x11)
.maxstack 2
.locals init (
[0] valuetype ConsoleApplication1.MyStruct s,
[1] int32 i
)
IL_0000: ldc.i4.0
IL_0001: stloc.1
IL_0002: br.s IL_0008
// loop start (head: IL_0008)
IL_0004: ldloc.1
IL_0005: ldc.i4.1
IL_0006: add
IL_0007: stloc.1
IL_0008: ldloc.1
IL_0009: ldc.i4 100000000
IL_000e: blt.s IL_0004
// end loop
IL_0010: ret
} // end of method Program::test1
the second loop
.method public hidebysig static
void test2 () cil managed
{
// Method begins at RVA 0x2074
// Code size 25 (0x19)
.maxstack 2
.locals init (
[0] valuetype [mscorlib]System.Nullable`1<valuetype ConsoleApplication1.MyStruct> s,
[1] int32 i
)
IL_0000: ldloca.s s
IL_0002: initobj valuetype [mscorlib]System.Nullable`1<valuetype ConsoleApplication1.MyStruct>
IL_0008: ldc.i4.0
IL_0009: stloc.1
IL_000a: br.s IL_0010
// loop start (head: IL_0010)
IL_000c: ldloc.1
IL_000d: ldc.i4.1
IL_000e: add
IL_000f: stloc.1
IL_0010: ldloc.1
IL_0011: ldc.i4 100000000
IL_0016: blt.s IL_000c
// end loop
IL_0018: ret
} // end of method Program::test2
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With