Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Casting struct to object for null comparison isn't causing boxing?

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?

like image 323
nawfal Avatar asked Apr 16 '13 11:04

nawfal


People also ask

Is struct nullable?

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.

Can a struct equal null?

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 .

What happens when we box or unbox nullable types?

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.


2 Answers

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 {}
}
like image 163
Matthew Watson Avatar answered Oct 12 '22 23:10

Matthew Watson


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
like image 25
Fredou Avatar answered Oct 12 '22 23:10

Fredou