Supporting Direct Handles to Boxed Value Types
A reader, Yves Dolce, writes:
Related to your last blog entry: The Astonishing S"Literal" String Type
I was surprised that sometimes, boxing is conceptually implicit...
That any Object method can be called directly without any explicit boxing.
And of course, explicit boxing is valid but seems less efficient as a real object ends up being created on the managed heap instead of just taking the address of the double (ldloca.s) ...
Console::WriteLine( S"2 cubed is {0}", result.ToString() ) ;
Console::WriteLine( S"2 cubed is {0}", __box(result) ) ;
gives
IL_0008: ldstr "2 cubed is {0}"
IL_000d: ldloca.s result
IL_000f: call instance string [mscorlib]System.Double::ToString()
IL_0014: call void [mscorlib]System.Console::WriteLine(string, object)
IL_002a: ldstr "2 cubed is {0}"
IL_002f: ldloc.0
IL_0030: box [mscorlib]System.Double
IL_0035: call void [mscorlib]System.Console::WriteLine(string, object)
Yes, the need to box occurs only when the source of the assignment to an Object^ is a value type. A Value type, recall, maintains its state within each associated object [what in C++ is created when we write T t]. A Reference type is a duple in which the named instance is a handle holding either null or the address of an unnamed object allocated on the managed heap. When we initialize the Object^ second parameter of Console::WriteLine with a Value type, there is a hiccup in the unified type system because there is no way to represent the Value type directly within the handle of a Reference type. The solution is to create a shadowed Reference to the Value type on the managed heap and pass in the address of that object. By invoking the ToString() method associated with the Value type, the need for boxing is elided because a String^ is a sealed Reference type.
I personally never bother to write ival.ToString() rather than ival when passing a value type to Console::WriteLine because generally speaking the overhead associated with a write operation is going to overwhelm the cost of the boxing itself, and any print operation is not going to be in a hot spot within an application. In general, why bother optimizing something for which the performance gain is negligible – particularly if it obfuscates the code, however slightly? [I know, one wants a steel discipline …]
In any case, this brings me to a second usage of the __box keyword that can provide performance gain by providing a direct handle to the boxed value type allocated within the managed heap. For example,
int main()
{
double result = 3.14159;
__box double * br = __box( result );
result = 2.7;
*br = 2.17;
Object * o = br;
Console::WriteLine( S"result :: {0}", result.ToString() ) ;
Console::WriteLine( S"result :: {0}", __box(result) ) ;
Console::WriteLine( S"result :: {0}", br );
}
Passing the boxed value type directly to Console::WriteLine eliminates both the boxing and the need to invoke ToString. [Of course, there is the earlier boxing to result to initialize br, so of course we don’t really gain anything unless we really put br to work.]
IL_0052: ldstr "result :: {0}"
IL_0057: ldloc.0
IL_0058: call void [mscorlib]System.Console::WriteLine(string, object)
In the revised language, the support for boxed value types is, in my opinion, considerably more elegant and integrated within the type system. Here is a translation of the function main() into C++/CLI [it’s machine generated; that’s why the spacing is different]:
using namespace stdcli::language;
int main()
{
double result = 3.14159;
double^ br = result;
result = 2.7;
*br = 2.17;
Object^ o = br;
Console::WriteLine( S"result :: {0}", result.ToString() );
Console::WriteLine( S"result :: {0}", result );
Console::WriteLine( S"result :: {0}", br );
}
I’ll talk about boxed value types some more in a future entry, or answer any questions. …