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

2

7

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?

Ketchup answered 16/4, 2013 at 11:8 Comment(4)
this code will not compile; Error 1 Use of unassigned local variable 's' for the second loopSpadix
@Spadix That's right. A typo in fact. I will update my answer with some benchmarking - I found a mistake in my timingsKetchup
this code, when compiled, seem to be running empty loop, you should post your actual benchmark code because i don't see how you can get 7500msSpadix
How can struct be null? DOES NOT COMPUTE!!!Rosetta
B
8

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 {}
}
Bustard answered 16/4, 2013 at 11:10 Comment(3)
@Spadix I've added some explicit test code to prevent that problem, so this demonstrates it properly.Bustard
in fact to replicate the behavior all you need is &= in the original code, no need to create the method that return the structSpadix
Ah cool, I didn't try that. I'll leave it like it is now tho. :)Bustard
S
3

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
Spadix answered 16/4, 2013 at 11:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.