.NET Generics and Code Bloat (or its lack thereof)

Introduction

I recently got questions from a couple of customers on the implications of using generic types (generics) in .NET on code bloat (also known as code explosion).

This is a legitimate concern and let me explain why.

Among the most popular programming languages, C++ was one of the first to provide generic programming through templates. And the way templates work in C++ is that, for each concrete type for which the template is instantiated, the code is actually duplicated: the templated arguments in the source of the templated class are replaced for every concrete type instantiation, and then compiled.

The obvious consequence is that the memory footprint for the templated type increases linearly with the number of instantiations of the templated type.

Another consequence is that the source code of the templated classes must be available to the compilation unit which instantiates them with concrete types. Usually this means defining all the templated types in header files only.

To be fair on this, it should be mentioned that there are techniques to reduce the code bloat with C++ templates. For example, common, non-templated functionality could be factored out in methods of a non-templated base class. Depending on the nature of the templated class (that is, how much of its functionality actually requires templated arguments and how much does not), this technique may reduce code bloat to some extent.

So you may wonder: what’s the story with .NET Generics? Well, read on….

.NET Generics: types, IL and JITted code

Generic types were introduced in .NET Framework version 2.0. They differ from templates in C++ in that their code is compiled to Intermediate Language (IL) code as such, that is as generic code (IL has specific opcodes for operations on generic types). At runtime, different instantiations generate different types (that is, different System.Type objects). And the generic type itself also has its own type.

This can be shown by this code:

 class MyGeneric<T, U>
{
    static void Main(string[] args)
    {
        Console.WriteLine(typeof(MyGeneric<,>).ToString());
        Console.WriteLine(typeof(MyGeneric<string,string>).ToString());
    }
}

 

which gives the following output:

 

 MyGenericsTest.MyGeneric`2[T,U]
MyGenericsTest.MyGeneric`2[System.String,System.String]

Note that at runtime a generic type’s name terminates with the “`” character and the generic’s arity, that is the number of its generic parameters.

What we said so far concerns the type system and the IL code, but what about the native code generated by the CLR JIT Compiler? As usual when we do not know, let’s use some low-level means to go and check ourselvessmile_regular. As you may have guessed by now, it will be the WinDbg debugger with the sos.dll debugger extension. But before doing that, a bit of background on some inner data structures of the CLR is in order.

One word of caution at this point: we will be inspecting some implementation details of generic types in .NET. While this can provide valuable information to developers who are interested in developing efficient code, these implementation details are not guaranteed to stay the same in all future versions of the framework. The analysis below is relative to version 2.0 of the CLR, that is version 2.x and 3.x of the .NET Framework.

MethodTable and MethodDesc

At runtime each .NET object points to its type information, which is divided into 2 parts: the MethodTable and the EEClass. The MethodTable, in turn, points to method descriptors, which describe methods for a type. Thankfully, we do not need to deal with the details of the memory layout of these data structures, because we can use some commands in the sos.dll debugger extension to dump them out. Let’s double-check this with an example:

 class Program
{
    static void Main(string[] args)    
    {
        Program p = new Program();
        p.Test();
    }

    void Test()
    {    
        Console.ReadLine();
    }
}

We open WinDbg, we attach to the running process which is waiting for input (see the Console.ReadLine() statement in the code) and we inspect those data structures

 0:003>  .loadby sos mscorwks0:003>  !name2ee mygenericstest.exe MyGenericsTest.Program
Module: 00462f2c (MyGenericsTest.exe)
Token: 0x02000006
MethodTable: 00463314
EEClass: 00461464
Name: MyGenericsTest.Program
0:003>  !dumpmt -md 00463314
EEClass: 00461464
Module: 00462f2c
Name: MyGenericsTest.Program
mdToken: 02000006  (D:\MSDNBlog\MyGenericsTest\bin\Debug\MyGenericsTest.exe)
BaseSize: 0xc
ComponentSize: 0x0
Number of IFaces in IFaceMap: 0
Slots in VTable: 7
--------------------------------------
MethodDesc Table
   Entry MethodDesc      JIT Name
6d386a90   6d201248   PreJIT System.Object.ToString()
6d386ab0   6d201250   PreJIT System.Object.Equals(System.Object)
6d386b20   6d201280   PreJIT System.Object.GetHashCode()
6d3f74c0   6d2012a4   PreJIT System.Object.Finalize()
005000d0   0046330c      JIT MyGenericsTest.Program..ctor()
00500070   004632f4      JIT MyGenericsTest.Program.Main(System.String[])
00500108   00463300      JIT MyGenericsTest.Program.Test()

Here you can see the content of the method table for the type Program. It contains methods explicitly defined (Main, Test) as well as methods inherited from System.Object.

The MethodDesc column reports the address of the Method Descriptor, which we can dump out:

 0:003>  !dumpmd 00463300
Method Name: MyGenericsTest.Program.Test()
Class: 00461464
MethodTable: 00463314
mdToken: 0600000d
Module: 00462f2c
IsJitted: yes
CodeAddr: 00500108

The isJitted field indicates whether the JIT Compiler already compiled the method. If so, the CodeAddr field contains the address of the JIT-ted code in memory.

Armed with these new tools, we are ready to investigate how the CLR generates native code for generic types.

Inspecting MethodTable and MethodDesc for instantiated generic types

The idea is very simple: we can look at method tables and method descriptors of instantiated generics at runtime and figure out to what extent the CLR can reuse code (thus avoiding code bloat) and to what extent it has to duplicate the code for different instantiated generic types.

Let’s start with this example: we define a generic class GenericClass<T>, and we instantiate it with 2 different reference types, A and B:

 class A: IEquatable<A>
{
    public bool  Equals(A other)
    {
        return object.ReferenceEquals(this, other);
    }
};

class B: IEquatable<B>
{
    public bool Equals(B other)
    {
        return object.ReferenceEquals(this, other);
    }
}

class GenericClass<T> where T : IEquatable<T>
{
    public GenericClass(T val)
    {
        m_p = val;
    }

    public bool TestEqual(T other)
    {
        return m_p.Equals(other);
    }

    private T m_p;
}

class Program
{
    static void Main(string[] args)
    {
        GenericClass<A> gca = new GenericClass<A>(new A());
        gca.TestEqual(default(A));
        GenericClass<B> gcb = new GenericClass<B>(new B());
        gcb.TestEqual(default(B));
        Console.ReadKey(true);
    }
}

and let’s dump out relevant data structures in the debugger:

 

 0:000>  !dumpheap -type GenericClass
 Address       MT     Size
01c01f20 00153664       12     
01c01f38 00153778       12     
total 2 objects
Statistics:
      MT    Count    TotalSize Class Name
00153778        1           12 GenericClass`1[[B, MyGenericsTest]]
00153664        1           12 GenericClass`1[[A, MyGenericsTest]]
Total 2 objects

0:000>  !dumpmt -md 00153664
EEClass: 00151728
Module: 00152f2c
Name: GenericClass`1[[A, MyGenericsTest]]
mdToken: 02000004  (D:\MSDNBlog\MyGenericsTest\bin\Debug\MyGenericsTest.exe)
BaseSize: 0xc
ComponentSize: 0x0
Number of IFaces in IFaceMap: 0
Slots in VTable: 6
--------------------------------------
MethodDesc Table
   Entry MethodDesc      JIT Name
6d386a90   6d201248   PreJIT System.Object.ToString()
6d386ab0   6d201250   PreJIT System.Object.Equals(System.Object)
6d386b20   6d201280   PreJIT System.Object.GetHashCode()
6d3f74c0   6d2012a4   PreJIT System.Object.Finalize()
005e01b8   001535e0      JIT GenericClass`1[[System.__Canon, mscorlib]]..ctor(System.__Canon)
005e0200   001535e8      JIT GenericClass`1[[System.__Canon, mscorlib]].TestEqual(System.__Canon)
0:000>  !dumpmt -md 00153778
EEClass: 00151728
Module: 00152f2c
Name: GenericClass`1[[B, MyGenericsTest]]
mdToken: 02000004  (D:\MSDNBlog\MyGenericsTest\bin\Debug\MyGenericsTest.exe)
BaseSize: 0xc
ComponentSize: 0x0
Number of IFaces in IFaceMap: 0
Slots in VTable: 6
--------------------------------------
MethodDesc Table
   Entry MethodDesc      JIT Name
6d386a90   6d201248   PreJIT System.Object.ToString()
6d386ab0   6d201250   PreJIT System.Object.Equals(System.Object)
6d386b20   6d201280   PreJIT System.Object.GetHashCode()
6d3f74c0   6d2012a4   PreJIT System.Object.Finalize()
005e01b8   001535e0      JIT GenericClass`1[[System.__Canon, mscorlib]]..ctor(System.__Canon)
005e0200   001535e8      JIT GenericClass`1[[System.__Canon, mscorlib]].TestEqual(System.__Canon)

 

We first searched for objects of type GenericClass<>. As expected, we found 2 (gca and gcb in the program above). They are of different types, GenericClass<A> and GenericClass<B>.

But when we dump out the method table we find out that the method descriptor for generic methods (constructor and TestEqual) are the same. This also implies that the JIT-ted code must be the same. Also note, in passing, the System.__Canon argument shown above. We’ll come to it later.

 

Can we infer a rule from this? Well, not yet. Let’s run the same program with an additional type, a value type this time

 ...
struct C: IEquatable<C>
{
    public bool Equals(C other)
    {
        return m_Val == other.m_Val;
    }

    public int m_Val;
}

class Program
{
    static void Main(string[] args)
    {
        GenericClass<A> gca = new GenericClass<A>(new A());
        gca.TestEqual(default(A));
        GenericClass<B> gcb = new GenericClass<B>(new B());
        gcb.TestEqual(default(B));
        GenericClass<C> gcc = new GenericClass<C>(new C());
        gcc.TestEqual(default(C));
        Console.ReadKey(true);
    }
}

and again let’s dump out the method tables:

 0:003>  !dumpheap -type GenericClass
 Address       MT     Size
01a11f20 00353664       12     
01a11f38 00353778       12     
01a11f44 003537f8       12     
total 3 objects
Statistics:
      MT    Count    TotalSize Class Name
003537f8        1           12 GenericClass`1[[C, MyGenericsTest]]
00353778        1           12 GenericClass`1[[B, MyGenericsTest]]
00353664        1           12 GenericClass`1[[A, MyGenericsTest]]

0:003>  !dumpmt -md 003537f8
EEClass: 00351840
Module: 00352f2c
Name: GenericClass`1[[C, MyGenericsTest]]
mdToken: 02000004  (D:\MSDNBlog\MyGenericsTest\bin\Debug\MyGenericsTest.exe)
BaseSize: 0xc
ComponentSize: 0x0
Number of IFaces in IFaceMap: 0
Slots in VTable: 6
--------------------------------------
MethodDesc Table
   Entry MethodDesc      JIT Name
6d386a90   6d201248   PreJIT System.Object.ToString()
6d386ab0   6d201250   PreJIT System.Object.Equals(System.Object)
6d386b20   6d201280   PreJIT System.Object.GetHashCode()
6d3f74c0   6d2012a4   PreJIT System.Object.Finalize()
00710388   003537e8      JIT GenericClass`1[[C, MyGenericsTest]]..ctor(C)
007103d0   003537f0      JIT GenericClass`1[[C, MyGenericsTest]].TestEqual(C)

0:003>  !dumpmt -md 00353664
EEClass: 00351728
Module: 00352f2c
Name: GenericClass`1[[A, MyGenericsTest]]
mdToken: 02000004  (D:\MSDNBlog\MyGenericsTest\bin\Debug\MyGenericsTest.exe)
BaseSize: 0xc
ComponentSize: 0x0
Number of IFaces in IFaceMap: 0
Slots in VTable: 6
--------------------------------------
MethodDesc Table
   Entry MethodDesc      JIT Name
6d386a90   6d201248   PreJIT System.Object.ToString()
6d386ab0   6d201250   PreJIT System.Object.Equals(System.Object)
6d386b20   6d201280   PreJIT System.Object.GetHashCode()
6d3f74c0   6d2012a4   PreJIT System.Object.Finalize()
007101b8   003535e0      JIT GenericClass`1[[System.__Canon, mscorlib]]..ctor(System.__Canon)
00710200   003535e8      JIT GenericClass`1[[System.__Canon, mscorlib]].TestEqual(System.__Canon)

Mmmh, here we have different method descriptors for GenericClass<A> and GenericClass<C>. For completeness’ sake, let’s also make sure that the native code they point to is also different:

 0:003>  !dumpmd 003537f0
Method Name: GenericClass`1[[C, MyGenericsTest]].TestEqual(C)
Class: 00351840
MethodTable: 003537f8
mdToken: 06000006
Module: 00352f2c
IsJitted: yes
CodeAddr: 007103d0
0:003>  !dumpmd 003535e8      
Method Name: GenericClass`1[[System.__Canon, mscorlib]].TestEqual(System.__Canon)
Class: 00351728
MethodTable: 003535fc
mdToken: 06000006
Module: 00352f2c
IsJitted: yes
CodeAddr: 00710200

What makes A and C different from each other, while A and B are not? Well, here we have one value type and one reference type. If we try with different value types (say GenericClass<C> and GenericClass<D>, where D is also a struct), we’ll find out that C and D are also different from each other (debugger output omitted here).

So a plausible inference from these tests is that the CLR produces one code for reference types, which is reused for all reference type instantiations, and different code for each value type instantiation. The reason is that reference types are all 4-byte (in 32-bit processes) or 8-byte (in 64-bit processes) values. So the layout of data types, and conequently of the code that accesses them, does not change. On the other hand, the layout of each value type is potentially unique. This is, indeed, the way the CLR works when JIT-ting code for generics. The blog post main purpose was not only, or may be even not so much, to establish this rule, but to show a technique to find this out

We mentioned the System.__Canon type shown as the type of generic functions. System.__Canon is an internal type which is used to make the canonical instantiation of a generic type. For code reuse purposes, the canonical instantiation provides the code for all reference-type instantiations.

Generics with Multiple Parameter Types

So far we have considered generic types with one parameter type only (arity == 1), but it is fairly common to have many. The .NET Framework itself have some, for example Dictionary<TKey, TValue>.

The rule can be easily extended to the case of arity > 1: if all the parameter types are reference types, the code is shared. Otherwise, it is not.

NGEN and Generics

A legitimate question at this point may be: what happens with pre-JITTed modules?

The short answer is that all the instantiations that are known at NGEN time, as well as the canonical instantiation, are compiled into the native image. Other instantiations are JIT-compiled at runtime.

Let’s show this once again with an experiment: with reference to the code above, let’s place A, B and C in one assembly, along with the Main() method which instantiates the generic type (be it MyGenericsTest.dll). When the assembly is ngen-ed, instantiation of GenericClass<A>, GenericClass<B> and GenericClass<C> are detected. Therefore, in addition to the canonical instantiation (which covers GenericClass<A> and GenericClass<B>), the methods for GenericClass<C> are also compiled into the native image MyGenericsTest.ni.dll.

 

 0:000> kL200
ChildEBP RetAddr  
002bee94 77028d94 ntdll!KiFastSystemCallRet
002bee98 77039522 ntdll!NtRequestWaitReplyPort+0xc
002beeb8 76267f1d ntdll!CsrClientCallServer+0xc2
002befa4 7626804d KERNEL32!GetConsoleInput+0xd2
002befc4 002ea61c KERNEL32!ReadConsoleInputA+0x1a
002bf04c 694a39d5 CLRStub[StubLinkStub]@221e5a4002ea61c
002bf0d4 66005639 mscorlib_ni!System.Console.ReadKey(Boolean)+0xa1
002bf118 66005911 MyGenericsTest_ni!GenericClass`1[[C, MyGenericsTest]].TestEqual(C)+0xfffffc95
002bf154 00a4008e MyGenericsTest_ni!Program1.Main(System.String[])+0x6d
002bf160 6ffb1b4c ConsoleApplication1!ConsoleApplication1.Program.Main(System.String[])+0x1e
002bf170 6ffc21b9 mscorwks!CallDescrWorker+0x33
002bf1f0 6ffd6531 mscorwks!CallDescrWorkerWithHandler+0xa3
002bf334 6ffd6564 mscorwks!MethodDesc::CallDescr+0x19c
002bf350 6ffd6582 mscorwks!MethodDesc::CallTargetWorker+0x1f
002bf368 7004784d mscorwks!MethodDescCallSite::Call+0x1a
002bf4cc 7004776d mscorwks!ClassLoader::RunMain+0x223
002bf734 70047cbd mscorwks!Assembly::ExecuteMainMethod+0xa6
002bfc04 70047ea7 mscorwks!SystemDomain::ExecuteMainMethod+0x456
002bfc54 70047dd7 mscorwks!ExecuteEXE+0x59
002bfc9c 70fa7c24 mscorwks!_CorExeMain+0x15c
002bfcac 76204911 mscoree!_CorExeMain+0x2c
002bfcb8 7700e4b6 KERNEL32!BaseThreadInitThunk+0xe
002bfcf8 7700e489 ntdll!__RtlUserThreadStart+0x23
002bfd10 00000000 ntdll!_RtlUserThreadStart+0x1b

0:000>  !address 66005639

                                     
Usage:                  Image
Allocation Base:        66000000
Base Address:           66004000
End Address:            66007000
Region Size:            00003000
Type:                   01000000    MEM_IMAGE
State:                  00001000    MEM_COMMIT
Protect:                00000020    PAGE_EXECUTE_READ
More info:              lmv m MyGenericsTest_ni
More info:              !lmi MyGenericsTest_ni
More info:              ln 0x66005639

0:000> kL200
ChildEBP RetAddr  
002beea4 77028d94 ntdll!KiFastSystemCallRet
002beea8 77039522 ntdll!NtRequestWaitReplyPort+0xc
002beec8 76267f1d ntdll!CsrClientCallServer+0xc2
002befb4 7626804d KERNEL32!GetConsoleInput+0xd2
002befd4 002ea61c KERNEL32!ReadConsoleInputA+0x1a
002bf05c 694a39d5 CLRStub[StubLinkStub]@221e5a4002ea61c
002bf0e4 66005a15 mscorlib_ni!System.Console.ReadKey(Boolean)+0xa1
002bf118 66005987 MyGenericsTest_ni!GenericClass`1[[C, MyGenericsTest]].TestEqual(C)+0x71
002bf154 00a4008e MyGenericsTest_ni!Program1.Main(System.String[])+0xe3
002bf160 6ffb1b4c ConsoleApplication1!ConsoleApplication1.Program.Main(System.String[])+0x1e
002bf170 6ffc21b9 mscorwks!CallDescrWorker+0x33
002bf1f0 6ffd6531 mscorwks!CallDescrWorkerWithHandler+0xa3
002bf334 6ffd6564 mscorwks!MethodDesc::CallDescr+0x19c
002bf350 6ffd6582 mscorwks!MethodDesc::CallTargetWorker+0x1f
002bf368 7004784d mscorwks!MethodDescCallSite::Call+0x1a
002bf4cc 7004776d mscorwks!ClassLoader::RunMain+0x223
002bf734 70047cbd mscorwks!Assembly::ExecuteMainMethod+0xa6
002bfc04 70047ea7 mscorwks!SystemDomain::ExecuteMainMethod+0x456
002bfc54 70047dd7 mscorwks!ExecuteEXE+0x59
002bfc9c 70fa7c24 mscorwks!_CorExeMain+0x15c
002bfcac 76204911 mscoree!_CorExeMain+0x2c
002bfcb8 7700e4b6 KERNEL32!BaseThreadInitThunk+0xe
002bfcf8 7700e489 ntdll!__RtlUserThreadStart+0x23
002bfd10 00000000 ntdll!_RtlUserThreadStart+0x1b

0:000>  !address 66005a15

                                     
Usage:                  Image
Allocation Base:        66000000
Base Address:           66004000
End Address:            66007000
Region Size:            00003000
Type:                   01000000    MEM_IMAGE
State:                  00001000    MEM_COMMIT
Protect:                00000020    PAGE_EXECUTE_READ
More info:              lmv m MyGenericsTest_ni
More info:              !lmi MyGenericsTest_ni
More info:              ln 0x66005a15

The commands above show that the code for the TestEqual method of both instantiatiations MyGenericClass<A> and MyGenericClass<C> is in the native image MyGenericTest.ni.dll.

Now let’s move the generic instantiation out of MyGenericsTest assembly, directly in Program.Main() in ConsoleApplication.exe. In this case, when MyGenericsTest.dll is NGENed, no instantiations are detected, so only the methods of the canonical instantiation are compiled into the native image. As a consequence, the code for the MyGenericsTest<C> instantiation is not in the native image and has to be JIT-compiled at runtime:

 0:000> kL200
ChildEBP RetAddr  
001bec20 77028d94 ntdll!KiFastSystemCallRet
001bec24 77039522 ntdll!NtRequestWaitReplyPort+0xc
001bec44 76267f1d ntdll!CsrClientCallServer+0xc2
001bed30 7626804d KERNEL32!GetConsoleInput+0xd2
001bed50 001da61c KERNEL32!ReadConsoleInputA+0x1a
001bedd8 694a39d5 CLRStub[StubLinkStub]@21be6a4001da61c
001bee60 6878557d mscorlib_ni!System.Console.ReadKey(Boolean)+0xa1
001beea4 00a400db MyGenericsTest_ni!GenericClass`1[[System.__Canon, mscorlib]].TestEqual(System.__Canon)+0x71
001beee0 6ffb1b4c ConsoleApplication1!ConsoleApplication1.Program.Main(System.String[])+0x6b
001beef0 6ffc21b9 mscorwks!CallDescrWorker+0x33
001bef70 6ffd6531 mscorwks!CallDescrWorkerWithHandler+0xa3
001bf0b4 6ffd6564 mscorwks!MethodDesc::CallDescr+0x19c
001bf0d0 6ffd6582 mscorwks!MethodDesc::CallTargetWorker+0x1f
001bf0e8 7004784d mscorwks!MethodDescCallSite::Call+0x1a
001bf24c 7004776d mscorwks!ClassLoader::RunMain+0x223
001bf4b4 70047cbd mscorwks!Assembly::ExecuteMainMethod+0xa6
001bf984 70047ea7 mscorwks!SystemDomain::ExecuteMainMethod+0x456
001bf9d4 70047dd7 mscorwks!ExecuteEXE+0x59
001bfa1c 70fa7c24 mscorwks!_CorExeMain+0x15c
001bfa2c 76204911 mscoree!_CorExeMain+0x2c
001bfa38 7700e4b6 KERNEL32!BaseThreadInitThunk+0xe
001bfa78 7700e489 ntdll!__RtlUserThreadStart+0x23
001bfa90 00000000 ntdll!_RtlUserThreadStart+0x1b
0:000>  !address 6878557d

                                     
Usage:                  Image
Allocation Base:        68780000
Base Address:           68784000
End Address:            68786000
Region Size:            00002000
Type:                   01000000    MEM_IMAGE
State:                  00001000    MEM_COMMIT
Protect:                00000020    PAGE_EXECUTE_READ
More info:              lmv m MyGenericsTest_ni
More info:              !lmi MyGenericsTest_ni
More info:              ln 0x6878557d

0:000> kL200
ChildEBP RetAddr  
001bec30 77028d94 ntdll!KiFastSystemCallRet
001bec34 77039522 ntdll!NtRequestWaitReplyPort+0xc
001bec54 76267f1d ntdll!CsrClientCallServer+0xc2
001bed40 7626804d KERNEL32!GetConsoleInput+0xd2
001bed60 001da61c KERNEL32!ReadConsoleInputA+0x1a
001bede8 694a39d5 CLRStub[StubLinkStub]@21be6a4001da61c
001bee70 00a4022b mscorlib_ni!System.Console.ReadKey(Boolean)+0xa1
001beea4 00a40153 MyGenericsTest_ni!GenericClass`1[[C, MyGenericsTest]].TestEqual(C)+0x6b
001beee0 6ffb1b4c ConsoleApplication1!ConsoleApplication1.Program.Main(System.String[])+0xe3
001beef0 6ffc21b9 mscorwks!CallDescrWorker+0x33
001bef70 6ffd6531 mscorwks!CallDescrWorkerWithHandler+0xa3
001bf0b4 6ffd6564 mscorwks!MethodDesc::CallDescr+0x19c
001bf0d0 6ffd6582 mscorwks!MethodDesc::CallTargetWorker+0x1f
001bf0e8 7004784d mscorwks!MethodDescCallSite::Call+0x1a
001bf24c 7004776d mscorwks!ClassLoader::RunMain+0x223
001bf4b4 70047cbd mscorwks!Assembly::ExecuteMainMethod+0xa6
001bf984 70047ea7 mscorwks!SystemDomain::ExecuteMainMethod+0x456
001bf9d4 70047dd7 mscorwks!ExecuteEXE+0x59
001bfa1c 70fa7c24 mscorwks!_CorExeMain+0x15c
001bfa2c 76204911 mscoree!_CorExeMain+0x2c
001bfa38 7700e4b6 KERNEL32!BaseThreadInitThunk+0xe
001bfa78 7700e489 ntdll!__RtlUserThreadStart+0x23
001bfa90 00000000 ntdll!_RtlUserThreadStart+0x1b
0:000>  !address 00a4022b

                                     
Usage:                  <unclassified>
Allocation Base:        00a40000
Base Address:           00a40000
End Address:            00a41000
Region Size:            00001000
Type:                   00020000    MEM_PRIVATE
State:                  00001000    MEM_COMMIT
Protect:                00000040    PAGE_EXECUTE_READWRITE

“!address 00a4022b” shows that the code of MyGenericsTest<C>.TestEqual() falls outside the range of the native image MyGenericsTest.ni.dll

We can use the !eeheap command to further demonstate that this is JIT-ted code:

 0:000>  !eeheap -loader
Loader Heap:
--------------------------------------
System Domain: 704fd058
LowFrequencyHeap: Size: 0x0(0)bytes.
HighFrequencyHeap: 002e2000(8000:1000) Size: 0x1000(4096)bytes.
StubHeap: 002ea000(2000:1000) Size: 0x1000(4096)bytes.
Virtual Call Stub Heap:
  IndcellHeap: Size: 0x0(0)bytes.
  LookupHeap: Size: 0x0(0)bytes.
  ResolveHeap: Size: 0x0(0)bytes.
  DispatchHeap: Size: 0x0(0)bytes.
  CacheEntryHeap: Size: 0x0(0)bytes.
Total size: 0x2000(8192)bytes
--------------------------------------
Shared Domain: 704fc9a8
LowFrequencyHeap: 00310000(2000:1000) Size: 0x1000(4096)bytes.
HighFrequencyHeap: Size: 0x0(0)bytes.
StubHeap: 0031a000(2000:1000) Size: 0x1000(4096)bytes.
Virtual Call Stub Heap:
  IndcellHeap: Size: 0x0(0)bytes.
  LookupHeap: Size: 0x0(0)bytes.
  ResolveHeap: 0032b000(5000:1000) Size: 0x1000(4096)bytes.
  DispatchHeap: 00327000(4000:1000) Size: 0x1000(4096)bytes.
  CacheEntryHeap: 00322000(3000:1000) Size: 0x1000(4096)bytes.
Total size: 0x4000(16384)bytes
--------------------------------------
Domain 1: 4435b8
LowFrequencyHeap: 002f0000(2000:2000) Size: 0x2000(8192)bytes.
HighFrequencyHeap: 002f2000(8000:2000) Size: 0x2000(8192)bytes.
StubHeap: Size: 0x0(0)bytes.
Virtual Call Stub Heap:
  IndcellHeap: 00300000(2000:1000) Size: 0x1000(4096)bytes.
  LookupHeap: 00306000(1000:1000) Size: 0x1000(4096)bytes.
  ResolveHeap: 0030a000(6000:1000) Size: 0x1000(4096)bytes.
  DispatchHeap: 00307000(3000:1000) Size: 0x1000(4096)bytes.
  CacheEntryHeap: Size: 0x0(0)bytes.
Total size: 0x8000(32768)bytes
--------------------------------------
Jit code heap:
LoaderCodeHeap: 00a40000(10000:1000) Size: 0x1000(4096)bytes.
Total size: 0x1000(4096)bytes
--------------------------------------
Module Thunk heaps:
Module 68dd1000: Size: 0x0(0)bytes.
Module 002f2c5c: Size: 0x0(0)bytes.
Module 66001000: Size: 0x0(0)bytes.
Total size: 0x0(0)bytes
--------------------------------------
Module Lookup Table heaps:
Module 68dd1000: Size: 0x0(0)bytes.
Module 002f2c5c: Size: 0x0(0)bytes.
Module 66001000: Size: 0x0(0)bytes.
Total size: 0x0(0)bytes
--------------------------------------
Total LoaderHeap size: 0xf000(61440)bytes
=======================================

Summary

The main purpose of this post  was to show how you can experiment yourself with some features of the CLR and find out, with the debugger, some of its inner workings.

By applying these techniques to generics, we have seen that:

  • The CLR goes a great deal in the direction of minimizing code bloat with generic programming. In particular, the CLR generates one implementation only for all reference types
  • When CLR code is pre-JITted through NGEN, all known instantiations are pre-JITTed to native code, in addition to the canonical instantiation that applies to all reference types. In other cases (value type instantiation unknown at NGEN time), the code is JIT-ted at runtime.