oscilatingcretin
oscilatingcretin

Reputation: 10959

Why does unboxing reference types perform worse when it's done with a helper method?

This is an overly simplified example of something else I am trying to do, but, for now, consider these casting methods:

public static string StringTryCast(object o)
{
    return o as string;
}

public static T RefTypeTryCast<T>(object o) where T : class
{
    return o as T;
}

When I execute those in a loop of 50,000,000 iterations, I seem to get much slower times than if I execute the cast inline. Here are the four tests I am conducting with a comment that corresponds to the test cases below them.

object BoxedValue = "my string";

//inline trycast
() => { s = BoxedValue as string; }

//method: RefTypeTryCast
() => { s = RefTypeTryCast<string>(BoxedValue); }

//method: StringTryCast
() => { s = StringTryCast(BoxedValue); }

Here are the test results. I ran five tests of 50,000,000 iterations for each method and then calculated the average.

inline trycast 50,000,000x...
  368 ms
  370 ms
  374 ms
  380 ms
  380 ms

  374.4 ms average over 5 iterations

method: RefTypeTryCast 50,000,000x...
  1083 ms
  1098 ms
  1100 ms
  1133 ms
  1138 ms

  1110.4 ms average over 5 iterations

method: StringTryCast 50,000,000x...
  477 ms
  478 ms
  487 ms
  489 ms
  493 ms

  484.8 ms average over 5 iterations

At 50,000,000 iterations, inline trycast is...
  1.2949 x Faster than method: StringTryCast
  2.9658 x Faster than method: RefTypeTryCast

I cannot understand why StringTryCast would perform any differently when it does an inline cast in a helper method. Adding [MethodImpl(MethodImplOptions.AggressiveInlining)] to the method didn't appear to help. Furthermore, RefTypeTryCast uses generics and performs 3x worse than inline.

It seems as though they should all perform relatively the same.

Edit: As mentioned in the comments, I use a helper class to run my tests. This is basically the encapsulated logic.

Stopwatch sw = new Stopwatch();

for (i = 0; i < 5; i++)
{
    sw.Restart();

    for (int o = 0; o < 50000000; o++)
    {
        Test(); //anon method passed in from lambda expression
    }

    sw.Stop();

    times.Add(i, sw.ElapsedMilliseconds);
}

Upvotes: 0

Views: 46

Answers (1)

Shadowed
Shadowed

Reputation: 966

This is for release build.

IL code for `StringTryCast` and `RefTypeTryCast`:
.method public hidebysig static !!T RefTypeTryCast<class T> (object o) cil managed 
{
    IL_0000: ldarg.0
    IL_0001: isinst !!T
    IL_0006: unbox.any !!T
    IL_000b: ret
}

.method public hidebysig static string StringTryCast (object o) cil managed 
{
    IL_0000: ldarg.0
    IL_0001: isinst [mscorlib]System.String
    IL_0006: ret
}

As you can see, there is one more instruction in case of generic function: unbox.any. From https://msdn.microsoft.com/en-us/library/system.reflection.emit.opcodes.unbox_any.aspx we can see that it does three operations: object is pushed to stack, popped and unboxed from stack and then pushed back to stack. So, there is more work done, hence more time.

For difference between inline and StringTryCast, in case of inline, you have one pushing to stack, checking if what's on stack is string and then popping from stack. For StringTryCast, there is pushing to stack, calling method which pushes argument to stack, checks if it's string, returns it pushing it from its stack to caller's stack and then when it's back, it is popped from stack once more. Again, more work -> more time.

Upvotes: 3

Related Questions