Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Runtime Representation of Generics — Part 2

DZone's Guide to

Runtime Representation of Generics — Part 2

·
Free Resource

This is an excerpt from Chapter 5 (Collections and Generics) of Pro .NET Performance, scheduled to appear in less than a month. I might be publishing a few more of these before and after the book is out.

After giving ample consideration to the design of Java generics and C++ templates, we can understand better the implementation choice for CLR generics. CLR generics are implemented as follows. Generic types — even open ones, like List<> — are first-class runtime citizens. There is a method table and an EEClass for each generic type and a System.Type instance can be produced as well. Generic types can be exported from assemblies and only a single definition of the generic type exists at compile-time. Generic types are not expanded at compile-time, but as we have seen the compiler makes sure that any operation attempted on generic type parameter instances is compatible with the specified generic constraints.

When the CLR needs to create an instance of a closed generic type, such as List<int>, it creates a method table and EEClass based on the open type. As always, the method table contains method pointers, which are compiled on the fly by the JIT compiler. However, there is a crucial optimization here: compiled method bodies on closed generic types that have reference type parameters can be shared. To digest this, let’s consider the List<T>.Add method and try to compile it to x86 instructions when T is a reference type:

//C# code:
public void Add(T item) {
  if (size < items.Length – 1) {
    items[size] = item;
    ++size;
  } else {
    AllocateAndAddSlow(item);
  }
} 
; x86 assembly when T is a reference type
; Assuming that ECX contains ‘this’ and EDX contains
; ‘item’, prologue and epilogue omitted
mov eax, dword ptr [ecx+4]       ; items
mov eax, dword ptr [eax+4]       ; items.Length
dec eax
cmp dword ptr [ecx+8], eax       ; size < items.Length – 1
jge AllocateAndAddSlow
mov eax, dword ptr [ecx+4]
mov ebx, dword ptr [ecx+8]
mov dword ptr [eax+4*ebx+4], edx ; items[size] = item
inc dword ptr [eax+8]            ; ++size

It’s clear that the method’s code does not depend on T in any way, and will work exactly as well for any reference type. This observation allows the JIT compiler to conserve resources (time and memory) and share the method table pointers for List<T>.Add in all method tables where T is a reference type.

(This idea requires some further refinement, which we will not carry out. For example, if the method’s body contained a new T[10] expression, it would require a separate code path for each T, or at least a way of obtaining T at runtime (through an additional hidden parameter passed to the method). Additionally, we haven’t considered how constraints affect the code generation — but you should be convinced by now that invoking interface methods or virtual methods through base classes requires the same code regardless of the type.)

The same idea does not work for value types. For example, when T is long, the assignment statement items[size] = item requires a different instruction, because 8 bytes must be copied instead of 4. Even larger value types may even require more than one instruction; and so on.

image

To demonstrate this in a simple setting, we can inspect using SOS the method tables of closed generic types that are all realizations of the same open generic type. For example, consider a BasicStack<T> class with only Push and Pop methods, as follows:

class BasicStack<T> {
  private T[] items;
  private int topIndex;
  public BasicStack(int capacity = 42) {
    items = new T[capacity];
  }
  public void Push(T item) {
    items[topIndex++] = item;
  }
  public T Pop() {
    return items[--topIndex];
  }
}

The method tables for BasicStack<string>, BasicStack<int[]>, BasicStack<int>, and BasicStack<double> are listed below. Note that the method table entries (i.e. code addresses) for the closed types with a reference type generic type argument are shared, and for the value types are not:

0:004> !dumpheap –stat
...
00173b40 1 16 BasicStack`1[[System.Double, mscorlib]]
00173a98 1 16 BasicStack`1[[System.Int32, mscorlib]]
00173a04 1 16 BasicStack`1[[System.Int32[], mscorlib]]
001739b0 1 16 BasicStack`1[[System.String, mscorlib]]
...

0:004> !dumpmt -md 001739b0
EEClass: 001714e0
Module: 00172e7c
Name: BasicStack`1[[System.String, mscorlib]]
...
00260360 00173924 JIT BasicStack`1[[System.__Canon, mscorlib]].Push(System.__Canon)
00260390 0017392c JIT BasicStack`1[[System.__Canon, mscorlib]].Pop()

0:004> !dumpmt -md 00173a04
EEClass: 001714e0
Module: 00172e7c
Name: BasicStack`1[[System.Int32[], mscorlib]]
...
00260360 00173924 JIT BasicStack`1[[System.__Canon, mscorlib]].Push(System.__Canon)
00260390 0017392c JIT BasicStack`1[[System.__Canon, mscorlib]].Pop()

0:004> !dumpmt -md 00173a98
EEClass: 0017158c
Module: 00172e7c
Name: BasicStack`1[[System.Int32, mscorlib]]
...
002603c0 00173a7c JIT BasicStack`1[[System.Int32, mscorlib]].Push(Int32)
002603f0 00173a84 JIT BasicStack`1[[System.Int32, mscorlib]].Pop()

0:004> !dumpmt -md 00173b40
EEClass: 001715ec
Module: 00172e7c
Name: BasicStack`1[[System.Double, mscorlib]]
...
00260420 00173b24 JIT BasicStack`1[[System.Double, mscorlib]].Push(Double)
00260458 00173b2c JIT BasicStack`1[[System.Double, mscorlib]].Pop()

Finally, if we inspect the actual method bodies, it becomes evident that the reference type versions do not depend at all on the actual type (all they do is move references around), and the value type versions do depend on the type. Copying an integer, after all, is different from copying a double floating-point number. Below are the disassembled versions of the Push method, with the line that actually moves the data around highlighted:

0:004> !u 00260360
Normal JIT generated code
BasicStack`1[[System.__Canon, mscorlib]].Push(System.__Canon)
00260360 57 push edi
00260361 56 push esi
00260362 8b7104 mov esi,dword ptr [ecx+4]
00260365 8b7908 mov edi,dword ptr [ecx+8]
00260368 8d4701 lea eax,[edi+1]
0026036b 894108 mov dword ptr [ecx+8],eax
0026036e 52 push edx
0026036f 8bce mov ecx,esi
00260371 8bd7 mov edx,edi
00260373 e8f4cb3870 call clr!JIT_Stelem_Ref (705ecf6c)
00260378 5e pop esi
00260379 5f pop edi
0026037a c3 ret

0:004> !u 002603c0
Normal JIT generated code
BasicStack`1[[System.Int32, mscorlib]].Push(Int32)
002603c0 57 push edi
002603c1 56 push esi
002603c2 8b7104 mov esi,dword ptr [ecx+4]
002603c5 8b7908 mov edi,dword ptr [ecx+8]
002603c8 8d4701 lea eax,[edi+1]
002603cb 894108 mov dword ptr [ecx+8],eax
002603ce 3b7e04 cmp edi,dword ptr [esi+4]
002603d1 7307 jae 002603da
002603d3 8954be08 mov dword ptr [esi+edi*4+8],edx
002603d7 5e pop esi
002603d8 5f pop edi
002603d9 c3 ret
002603da e877446170 call clr!JIT_RngChkFail (70874856)
002603df cc int 3

0:004> !u 00260420
Normal JIT generated code
BasicStack`1[[System.Double, mscorlib]].Push(Double)
00260420 56 push esi
00260421 8b5104 mov edx,dword ptr [ecx+4]
00260424 8b7108 mov esi,dword ptr [ecx+8]
00260427 8d4601 lea eax,[esi+1]
0026042a 894108 mov dword ptr [ecx+8],eax
0026042d 3b7204 cmp esi,dword ptr [edx+4]
00260430 730c jae 0026043e
00260432 dd442408 fld qword ptr [esp+8]
00260436 dd5cf208 fstp qword ptr [edx+esi*8+8]
0026043a 5e pop esi
0026043b c20800 ret 8
0026043e e813446170 call clr!JIT_RngChkFail (70874856)
00260443 cc int 3

We have already seen that the .NET generics implementation is fully type-safe at compile-time. It only remains to ascertain that there is no boxing cost incurred when using value types with generic collections. Indeed, because the JIT compiler compiles a separate method body for each closed generic type where the generic type arguments are value types, there is no need for boxing.

To summarize, .NET generics offer significant advantages when contrasted with Java generics or C++ templates. The generic constraints mechanism is somewhat limited compared to the Wild West that C++ affords, but the flexibility gains from sharing generic types across assemblies and the performance benefits from generating code on demand (and sharing it) are overwhelming.


I am posting short updates and links on Twitter as well as on this blog. You can follow me: @goldshtn
Topics:

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}