C# to CIL Boxing vs. ToString Cost
Asked Answered
H

2

6

I'm reading the book CLR via C# (4th edition), not as a newcomer to C# but as someone who knows the language trying to improve my grasp on the underlying functionality of the CLR.

Anyway, in this book an example is given (pg127-131) when discussing boxing/unboxing of value types which ends with a call to Console.WriteLine with a value type being concatenated to a string being passed as the argument.

The book explains that boxing and unboxing/copy operations cause overhead, which I already knew, but it then stated that the example could be optimized by running .ToString() on the value type being passed in.

I created an example program and compiled it, then used ILDASM to inspect the IL it generated. The version with ToString essentially is identical, but replaces the "box" instruction with a "call" to ToString (no shock there).

I benchmarked the code in a loop of 100000 runs, and there was no difference (it fluctuated which one was faster). I realize that other factors come into play when benchmarking (caches, etc.), but by the way the book explained it I had expected to see a significant difference when avoiding the "box" instruction even in a naieve benchmark..

Is it just that calling a function isn't much better? Is there a boxing operation going on in the ToString that nullifies the benefits and the book is wrong? Can someone shed some light on this?

For reference, here are the two ILDASM readouts:


.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       24 (0x18)
  .maxstack  2
  .locals init (int32 V_0)
  IL_0000:  ldc.i4.4
  IL_0001:  stloc.0
  IL_0002:  ldloc.0
  IL_0003:  box        [mscorlib]System.Int32
  IL_0008:  ldstr      "."
  IL_000d:  call       string [mscorlib]System.String::Concat(object,
                                                              object)
  IL_0012:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_0017:  ret
} // end of method Program::Main

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       25 (0x19)
  .maxstack  2
  .locals init (int32 V_0)
  IL_0000:  ldc.i4.4
  IL_0001:  stloc.0
  IL_0002:  ldloca.s   V_0
  IL_0004:  call       instance string [mscorlib]System.Int32::ToString()
  IL_0009:  ldstr      "."
  IL_000e:  call       string [mscorlib]System.String::Concat(string,
                                                              string)
  IL_0013:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_0018:  ret
} // end of method Program::Main
Heteroecious answered 11/3, 2015 at 19:30 Comment(1)
Look at the disassembly window and see what the difference in the x86 assembly code is.Yila
C
5

The CLR is likely inlining the call to string.Concat(object,object) which results in the same code as your "optimized" version. Note that the C# compiler will leave a lot of these kinds of optimizations up to the CLR, since it has better tools available to do them.

Other than a couple null checks (which would be optimized out) it simply calls string.Concat(left.ToString(),right.ToString()) which would be simplified to string.Concat(left,right.ToString()) since the CLR would see that ToString() just returns this.

Thus the executing code is likely identical in both cases.

Contemptible answered 11/3, 2015 at 19:42 Comment(1)
Makes sense - I was focusing too much on the IL.Heteroecious
A
0

You're missing the fact, that String.Concat will call ToString on provided object argument internally:

public static String Concat(Object arg0, Object arg1) {
    Contract.Ensures(Contract.Result<String>() != null);
    Contract.EndContractBlock();

    if (arg0 == null)
    {
        arg0 = String.Empty;
    }

    if (arg1==null) {
        arg1 = String.Empty;
    }
    return Concat(arg0.ToString(), arg1.ToString());
}

So the call instruction will be there anyway, but it's hidden from you inside Concat method call.

Calling ToString makes different Concat overload be picked, and this one will not call ToString internally.

IL_000d:  call       string [mscorlib]System.String::Concat(object, object)

vs.

IL_000e:  call       string [mscorlib]System.String::Concat(string, string)
Aeromarine answered 11/3, 2015 at 19:41 Comment(2)
The call is always there but there is an additional boxing operation.Montgolfier
@Montgolfier Yes, that's exactly what's going on.Aeromarine

© 2022 - 2024 — McMap. All rights reserved.