Consider the following code fragment:
Module Module1 Sub Foo(Of T)(ByVal x As T) If x Is Nothing Then End If End Sub Sub Main() Foo(10) End SubEnd Module
The first thing you might ask is : "hey, how come the compiler allows me to check for Nothing? Isn't that a reference comparison? There is no constraint on T, so why does the compiler allow you to do this?" The simple answer is : it makes your life easier. Imagine that the compiler did not allow this : you now have to write two versions of every generic method : one that assumes the parameter can be Nothing and thus must make a null-check, and one that assumes the parameter must have a value. That really sucks.
Ok, but what code does the compiler generate? Here's the bit of IL code that's interesting:
IL_0001: ldarg.0IL_0002: box !!TIL_0007: ldnullIL_0008: ceq
Ah ha! The compiler treats x as a value type, and emits a box instruction. The it emits code that checks against null : since we've now boxed x, this pushes two reference types onto the stack for ceq to operate on.
One way therefore to think about this, is that when a valuetype is passed to a method that takes a generic argument which is not constrained, the compiler will emit box instructions to make them reference types. Indeed, if you take a look at the number of managed objects, you'll definitely see that when you call Foo with a value type, an extra object is created on the heap (to see this, use the !dumpheap instruction in Visual Studio with SOS loaded - check this for more information). But when you call Foo with a reference type, no object is created on the heap.
Why does this happen? Let's take a look at how the CLR jits the above IL.
Here's the retail x86:
00000000 push edi 00000001 push esi 00000002 mov edi,ecx 00000004 cmp dword ptr ds:[007795E0h],0 0000000b je 00000012 0000000d call 79826E47 00000012 mov ecx,79102290h 00000017 call FFE60B1C 0000001c mov esi,eax 0000001e mov dword ptr [esi+4],edi 00000021 test esi,esi 00000023 jne 0000003B
There, definitely, the first call is constructing the boxed value, and the second call is calling the managed helper to test object equivalence.
Here's the version of retail x86 that's generated when Foo is called with a reference type:
00000000 push esi 00000001 mov esi,ecx 00000003 cmp dword ptr ds:[007795E0h],0 0000000a je 00000011 0000000c call 79826D47 00000011 test esi,esi 00000013 jne 0000001C
See, no extra call to construct an object on the heap.
Next time, I hope to post about some of the changes we are considering making for the next release.