If you have worked with .NET much at all, then you have undoubtedly used String.Format. It is one of those essential functions that just makes development a lot simpler. However, I have found that a lot of people don't realize they are introducing a boxing operation when passing value types (byte, int, double, etc) as parameters to String.Format.
If you look closely at the signature of the String.Format method, you will notice that the arguments passed for formatting are of type object. When a value type is passed as one of the object parameters, it requires a boxing operation. In case you didn't know, boxing is a mechanism to convert a value type to a reference type. Value types are stored on the stack, but reference types are allocated on the managed heap. In order for a value type to be used as an object, the runtime must allocate memory on the heap. The value type is then copied from the stack to the heap. This results in a reference to the value on the heap (just like any other reference type). There is also an operation known as unboxing, which is simply the opposite (copying from the heap to the stack).
While boxing can be very convenient when you want to use a value type as a reference type, it can introduce performance degradation. In some situations, the performance variation may be negligible. However, if you aren't judicious with the manner in which it is used, then you may experience unnecessarily poor performance.
Here is a seemingly innocent method that uses String.Format:
public void ProcessOrder(int orderID)
{
string message = String.Format("Started Processing Order {0}.", orderID);
// Log Message
// Process the Order
}However, examining the generated IL reveals that the integer causes a boxing operation:
.method public hidebysig instance void ProcessOrder(int32 orderID) cil managed
{
// Code size 19 (0x13)
.maxstack 2
.locals init ([0] string message)
IL_0000: nop
IL_0001: ldstr "Started Processing Order {0}."
IL_0006: ldarg.1
IL_0007: box [mscorlib]System.Int32
IL_000c: call string [mscorlib]System.String::Format(string,
object)
IL_0011: stloc.0
IL_0012: ret
}Fortunately, it is very easy to avoid the boxing operation by using the ToString method on the integer parameter. Here is our method with the very simple addition of the ToString method:
public void ProcessOrder(int orderID)
{
string message = String.Format("Started Processing Order {0}.", orderID.ToString());
// Log Message
// Process the Order
}After reviewing the updated IL, you can see that the boxing operation is avoided since a string (a reference type) is being passed rather than an integer (a value type).
.method public hidebysig instance void ProcessOrder(int32 orderID) cil managed
{
// Code size 20 (0x14)
.maxstack 2
.locals init ([0] string message)
IL_0000: nop
IL_0001: ldstr "Started Processing Order {0}."
IL_0006: ldarga.s orderID
IL_0008: call instance string [mscorlib]System.Int32::ToString()
IL_000d: call string [mscorlib]System.String::Format(string,
object)
IL_0012: stloc.0
IL_0013: ret
}
The instantiation of a string is more efficient than the boxing operation. It is directly created on the heap as opposed to copying the value from the stack (boxing of the integer).
It should be noted that this also applies to similar method overloads such as Console.WriteLine.
While this particular example may be a minimal performance difference, it is still important to understand the consequences of boxing and be able to recognize situations when it occurs.