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….
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()); }}
MyGenericsTest.MyGeneric`2[T,U]MyGenericsTest.MyGeneric`2[System.String,System.String]
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.ProgramModule: 00462f2c (MyGenericsTest.exe)Token: 0x02000006MethodTable: 00463314EEClass: 00461464Name: MyGenericsTest.Program0:003> !dumpmt -md 00463314EEClass: 00461464Module: 00462f2cName: MyGenericsTest.ProgrammdToken: 02000006 (D:\MSDNBlog\MyGenericsTest\bin\Debug\MyGenericsTest.exe)BaseSize: 0xcComponentSize: 0x0Number of IFaces in IFaceMap: 0Slots in VTable: 7--------------------------------------MethodDesc Table Entry MethodDesc JIT Name6d386a90 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()
0:003> .loadby sos mscorwks
0:003> !name2ee mygenericstest.exe MyGenericsTest.ProgramModule: 00462f2c (MyGenericsTest.exe)Token: 0x02000006MethodTable: 00463314EEClass: 00461464Name: MyGenericsTest.Program0:003> !dumpmt -md 00463314EEClass: 00461464Module: 00462f2cName: MyGenericsTest.ProgrammdToken: 02000006 (D:\MSDNBlog\MyGenericsTest\bin\Debug\MyGenericsTest.exe)BaseSize: 0xcComponentSize: 0x0Number of IFaces in IFaceMap: 0Slots in VTable: 7--------------------------------------MethodDesc Table Entry MethodDesc JIT Name6d386a90 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 00463300Method Name: MyGenericsTest.Program.Test()Class: 00461464MethodTable: 00463314mdToken: 0600000dModule: 00462f2cIsJitted: yesCodeAddr: 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.
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); }}
0:000> !dumpheap -type GenericClass Address MT Size01c01f20 00153664 12 01c01f38 00153778 12 total 2 objectsStatistics: MT Count TotalSize Class Name00153778 1 12 GenericClass`1[[B, MyGenericsTest]]00153664 1 12 GenericClass`1[[A, MyGenericsTest]]Total 2 objects0:000> !dumpmt -md 00153664EEClass: 00151728Module: 00152f2cName: GenericClass`1[[A, MyGenericsTest]]mdToken: 02000004 (D:\MSDNBlog\MyGenericsTest\bin\Debug\MyGenericsTest.exe)BaseSize: 0xcComponentSize: 0x0Number of IFaces in IFaceMap: 0Slots in VTable: 6--------------------------------------MethodDesc Table Entry MethodDesc JIT Name6d386a90 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 00153778EEClass: 00151728Module: 00152f2cName: GenericClass`1[[B, MyGenericsTest]]mdToken: 02000004 (D:\MSDNBlog\MyGenericsTest\bin\Debug\MyGenericsTest.exe)BaseSize: 0xcComponentSize: 0x0Number of IFaces in IFaceMap: 0Slots in VTable: 6--------------------------------------MethodDesc Table Entry MethodDesc JIT Name6d386a90 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)
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 Size01a11f20 00353664 12 01a11f38 00353778 12 01a11f44 003537f8 12 total 3 objectsStatistics: MT Count TotalSize Class Name003537f8 1 12 GenericClass`1[[C, MyGenericsTest]]00353778 1 12 GenericClass`1[[B, MyGenericsTest]]00353664 1 12 GenericClass`1[[A, MyGenericsTest]]0:003> !dumpmt -md 003537f8EEClass: 00351840Module: 00352f2cName: GenericClass`1[[C, MyGenericsTest]]mdToken: 02000004 (D:\MSDNBlog\MyGenericsTest\bin\Debug\MyGenericsTest.exe)BaseSize: 0xcComponentSize: 0x0Number of IFaces in IFaceMap: 0Slots in VTable: 6--------------------------------------MethodDesc Table Entry MethodDesc JIT Name6d386a90 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 00353664EEClass: 00351728Module: 00352f2cName: GenericClass`1[[A, MyGenericsTest]]mdToken: 02000004 (D:\MSDNBlog\MyGenericsTest\bin\Debug\MyGenericsTest.exe)BaseSize: 0xcComponentSize: 0x0Number of IFaces in IFaceMap: 0Slots in VTable: 6--------------------------------------MethodDesc Table Entry MethodDesc JIT Name6d386a90 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 003537f0Method Name: GenericClass`1[[C, MyGenericsTest]].TestEqual(C)Class: 00351840MethodTable: 003537f8mdToken: 06000006Module: 00352f2cIsJitted: yesCodeAddr: 007103d00:003> !dumpmd 003535e8 Method Name: GenericClass`1[[System.__Canon, mscorlib]].TestEqual(System.__Canon)Class: 00351728MethodTable: 003535fcmdToken: 06000006Module: 00352f2cIsJitted: yesCodeAddr: 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
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.
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> kL200ChildEBP RetAddr 002bee94 77028d94 ntdll!KiFastSystemCallRet002bee98 77039522 ntdll!NtRequestWaitReplyPort+0xc002beeb8 76267f1d ntdll!CsrClientCallServer+0xc2002befa4 7626804d KERNEL32!GetConsoleInput+0xd2002befc4 002ea61c KERNEL32!ReadConsoleInputA+0x1a002bf04c 694a39d5 CLRStub[StubLinkStub]@221e5a4002ea61c002bf0d4 66005639 mscorlib_ni!System.Console.ReadKey(Boolean)+0xa1002bf118 66005911 MyGenericsTest_ni!GenericClass`1[[C, MyGenericsTest]].TestEqual(C)+0xfffffc95002bf154 00a4008e MyGenericsTest_ni!Program1.Main(System.String[])+0x6d002bf160 6ffb1b4c ConsoleApplication1!ConsoleApplication1.Program.Main(System.String[])+0x1e002bf170 6ffc21b9 mscorwks!CallDescrWorker+0x33002bf1f0 6ffd6531 mscorwks!CallDescrWorkerWithHandler+0xa3002bf334 6ffd6564 mscorwks!MethodDesc::CallDescr+0x19c002bf350 6ffd6582 mscorwks!MethodDesc::CallTargetWorker+0x1f002bf368 7004784d mscorwks!MethodDescCallSite::Call+0x1a002bf4cc 7004776d mscorwks!ClassLoader::RunMain+0x223002bf734 70047cbd mscorwks!Assembly::ExecuteMainMethod+0xa6002bfc04 70047ea7 mscorwks!SystemDomain::ExecuteMainMethod+0x456002bfc54 70047dd7 mscorwks!ExecuteEXE+0x59002bfc9c 70fa7c24 mscorwks!_CorExeMain+0x15c002bfcac 76204911 mscoree!_CorExeMain+0x2c002bfcb8 7700e4b6 KERNEL32!BaseThreadInitThunk+0xe002bfcf8 7700e489 ntdll!__RtlUserThreadStart+0x23002bfd10 00000000 ntdll!_RtlUserThreadStart+0x1b0:000> !address 66005639 Usage: ImageAllocation Base: 66000000Base Address: 66004000End Address: 66007000Region Size: 00003000Type: 01000000 MEM_IMAGEState: 00001000 MEM_COMMITProtect: 00000020 PAGE_EXECUTE_READMore info: lmv m MyGenericsTest_niMore info: !lmi MyGenericsTest_niMore info: ln 0x660056390:000> kL200ChildEBP RetAddr 002beea4 77028d94 ntdll!KiFastSystemCallRet002beea8 77039522 ntdll!NtRequestWaitReplyPort+0xc002beec8 76267f1d ntdll!CsrClientCallServer+0xc2002befb4 7626804d KERNEL32!GetConsoleInput+0xd2002befd4 002ea61c KERNEL32!ReadConsoleInputA+0x1a002bf05c 694a39d5 CLRStub[StubLinkStub]@221e5a4002ea61c002bf0e4 66005a15 mscorlib_ni!System.Console.ReadKey(Boolean)+0xa1002bf118 66005987 MyGenericsTest_ni!GenericClass`1[[C, MyGenericsTest]].TestEqual(C)+0x71002bf154 00a4008e MyGenericsTest_ni!Program1.Main(System.String[])+0xe3002bf160 6ffb1b4c ConsoleApplication1!ConsoleApplication1.Program.Main(System.String[])+0x1e002bf170 6ffc21b9 mscorwks!CallDescrWorker+0x33002bf1f0 6ffd6531 mscorwks!CallDescrWorkerWithHandler+0xa3002bf334 6ffd6564 mscorwks!MethodDesc::CallDescr+0x19c002bf350 6ffd6582 mscorwks!MethodDesc::CallTargetWorker+0x1f002bf368 7004784d mscorwks!MethodDescCallSite::Call+0x1a002bf4cc 7004776d mscorwks!ClassLoader::RunMain+0x223002bf734 70047cbd mscorwks!Assembly::ExecuteMainMethod+0xa6002bfc04 70047ea7 mscorwks!SystemDomain::ExecuteMainMethod+0x456002bfc54 70047dd7 mscorwks!ExecuteEXE+0x59002bfc9c 70fa7c24 mscorwks!_CorExeMain+0x15c002bfcac 76204911 mscoree!_CorExeMain+0x2c002bfcb8 7700e4b6 KERNEL32!BaseThreadInitThunk+0xe002bfcf8 7700e489 ntdll!__RtlUserThreadStart+0x23002bfd10 00000000 ntdll!_RtlUserThreadStart+0x1b0:000> !address 66005a15 Usage: ImageAllocation Base: 66000000Base Address: 66004000End Address: 66007000Region Size: 00003000Type: 01000000 MEM_IMAGEState: 00001000 MEM_COMMITProtect: 00000020 PAGE_EXECUTE_READMore info: lmv m MyGenericsTest_niMore info: !lmi MyGenericsTest_niMore 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> kL200ChildEBP RetAddr 001bec20 77028d94 ntdll!KiFastSystemCallRet001bec24 77039522 ntdll!NtRequestWaitReplyPort+0xc001bec44 76267f1d ntdll!CsrClientCallServer+0xc2001bed30 7626804d KERNEL32!GetConsoleInput+0xd2001bed50 001da61c KERNEL32!ReadConsoleInputA+0x1a001bedd8 694a39d5 CLRStub[StubLinkStub]@21be6a4001da61c001bee60 6878557d mscorlib_ni!System.Console.ReadKey(Boolean)+0xa1001beea4 00a400db MyGenericsTest_ni!GenericClass`1[[System.__Canon, mscorlib]].TestEqual(System.__Canon)+0x71001beee0 6ffb1b4c ConsoleApplication1!ConsoleApplication1.Program.Main(System.String[])+0x6b001beef0 6ffc21b9 mscorwks!CallDescrWorker+0x33001bef70 6ffd6531 mscorwks!CallDescrWorkerWithHandler+0xa3001bf0b4 6ffd6564 mscorwks!MethodDesc::CallDescr+0x19c001bf0d0 6ffd6582 mscorwks!MethodDesc::CallTargetWorker+0x1f001bf0e8 7004784d mscorwks!MethodDescCallSite::Call+0x1a001bf24c 7004776d mscorwks!ClassLoader::RunMain+0x223001bf4b4 70047cbd mscorwks!Assembly::ExecuteMainMethod+0xa6001bf984 70047ea7 mscorwks!SystemDomain::ExecuteMainMethod+0x456001bf9d4 70047dd7 mscorwks!ExecuteEXE+0x59001bfa1c 70fa7c24 mscorwks!_CorExeMain+0x15c001bfa2c 76204911 mscoree!_CorExeMain+0x2c001bfa38 7700e4b6 KERNEL32!BaseThreadInitThunk+0xe001bfa78 7700e489 ntdll!__RtlUserThreadStart+0x23001bfa90 00000000 ntdll!_RtlUserThreadStart+0x1b0:000> !address 6878557d Usage: ImageAllocation Base: 68780000Base Address: 68784000End Address: 68786000Region Size: 00002000Type: 01000000 MEM_IMAGEState: 00001000 MEM_COMMITProtect: 00000020 PAGE_EXECUTE_READMore info: lmv m MyGenericsTest_niMore info: !lmi MyGenericsTest_niMore info: ln 0x6878557d0:000> kL200ChildEBP RetAddr 001bec30 77028d94 ntdll!KiFastSystemCallRet001bec34 77039522 ntdll!NtRequestWaitReplyPort+0xc001bec54 76267f1d ntdll!CsrClientCallServer+0xc2001bed40 7626804d KERNEL32!GetConsoleInput+0xd2001bed60 001da61c KERNEL32!ReadConsoleInputA+0x1a001bede8 694a39d5 CLRStub[StubLinkStub]@21be6a4001da61c001bee70 00a4022b mscorlib_ni!System.Console.ReadKey(Boolean)+0xa1001beea4 00a40153 MyGenericsTest_ni!GenericClass`1[[C, MyGenericsTest]].TestEqual(C)+0x6b001beee0 6ffb1b4c ConsoleApplication1!ConsoleApplication1.Program.Main(System.String[])+0xe3001beef0 6ffc21b9 mscorwks!CallDescrWorker+0x33001bef70 6ffd6531 mscorwks!CallDescrWorkerWithHandler+0xa3001bf0b4 6ffd6564 mscorwks!MethodDesc::CallDescr+0x19c001bf0d0 6ffd6582 mscorwks!MethodDesc::CallTargetWorker+0x1f001bf0e8 7004784d mscorwks!MethodDescCallSite::Call+0x1a001bf24c 7004776d mscorwks!ClassLoader::RunMain+0x223001bf4b4 70047cbd mscorwks!Assembly::ExecuteMainMethod+0xa6001bf984 70047ea7 mscorwks!SystemDomain::ExecuteMainMethod+0x456001bf9d4 70047dd7 mscorwks!ExecuteEXE+0x59001bfa1c 70fa7c24 mscorwks!_CorExeMain+0x15c001bfa2c 76204911 mscoree!_CorExeMain+0x2c001bfa38 7700e4b6 KERNEL32!BaseThreadInitThunk+0xe001bfa78 7700e489 ntdll!__RtlUserThreadStart+0x23001bfa90 00000000 ntdll!_RtlUserThreadStart+0x1b0:000> !address 00a4022b Usage: <unclassified>Allocation Base: 00a40000Base Address: 00a40000End Address: 00a41000Region Size: 00001000Type: 00020000 MEM_PRIVATEState: 00001000 MEM_COMMITProtect: 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 -loaderLoader Heap:--------------------------------------System Domain: 704fd058LowFrequencyHeap: 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: 704fc9a8LowFrequencyHeap: 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: 4435b8LowFrequencyHeap: 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=======================================
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: