Generics under the hood

and a JITter bug for dessert

by Alexandr Nikitin

Overview

  • Compare with JAVA and C++
  • Find out .NET is awesome
  • Recall memory layout
  • Look under the hood
  • Analyze the performance degradation
  • Discuss some interesting things

JAVA and swear words

“Šuo ir kariamas pripranta.”

Lithuanian proverbs

Generics in .NET

awesom-o

Well known benefits

  • Reduce code duplication
  • Constraints & variance
  • Performance improvements (no boxing, no casting)
  • Compile-time checks

.NET memory layout


var o = new object();
                        

Instance on heap:

Sync block address
Method table address
Field1
FieldN

Method Table structure

  • The central data structure of the runtime
  • "Hot" data


EEClass address
Interface Map Table address
Inherited Virtual Method addresses
Introduced Virtual Method addresses
Instance Method addresses
Static Method addresses
Static Fields values
InterfaceN method addresses

EEClass structure

  • Everything about the type
  • Data needed by type loading, JITing or reflection
  • "Cold" data
  • Too complex: contains EEClassOptionalFields, EEClassPackedFields

WinDbg the Great and Powerful

SOS / Son of Strike

SOSEX / SOS extensions

An example class:


public class MyClass
{
    private int _myField;

    public int MyMethod()
    {
        return _myField;
    }
}
                        

An instance:


var myClass = new MyClass();
                            

0:003> !DumpHeap -type GenericsUnderTheHood.MyClass
Address          MT                     Size
0000004a2d912de8 00007fff8e7540d8       24

Statistics:
MT                      Count       TotalSize   Class Name
00007fff8e7540d8        1           24          GenericsUnderTheHood.MyClass
Total 1 objects
                            

Method Table looks like:


0:003> !dumpmt -md 00007fff8e7540d8
EEClass:         00007fff8e8623f0
Module:          00007fff8e752fc8
Name:            GenericsUnderTheHood.MyClass
mdToken:         0000000002000002
File:            C:\Projects\my\GenericsUnderTheHood\GenericsUnderTheHood\bin\Debug\GenericsUnderTheHood.exe
BaseSize:        0x18
ComponentSize:   0x0
Slots in VTable: 6
Number of IFaces in IFaceMap: 0
--------------------------------------
MethodDesc Table
Entry            MethodDesc       JIT    Name
00007fffecc86300 00007fffec8380e8 PreJIT System.Object.ToString()
00007fffeccce760 00007fffec8380f0 PreJIT System.Object.Equals(System.Object)
00007fffeccd1ad0 00007fffec838118 PreJIT System.Object.GetHashCode()
00007fffeccceb50 00007fffec838130 PreJIT System.Object.Finalize()
00007fff8e8701c0 00007fff8e7540d0    JIT GenericsUnderTheHood.MyClass..ctor()
00007fff8e75c048 00007fff8e7540c0   NONE GenericsUnderTheHood.MyClass.MyMethod()
                            

EEClass looks like:


0:003> !DumpClass 00007fff8e8623f0
Class Name:      GenericsUnderTheHood.MyClass
mdToken:         0000000002000002
File:            C:\Projects\my\GenericsUnderTheHood\GenericsUnderTheHood\bin\Debug\GenericsUnderTheHood.exe
Parent Class:    00007fffec824908
Module:          00007fff8e752fc8
Method Table:    00007fff8e7540d8
Vtable Slots:    4
Total Method Slots:  5
Class Attributes:    100001
Transparency:        Critical
NumInstanceFields:   1
NumStaticFields:     0
MT                Field          Offset    Type          VT Attr     Value Name
00007fffecf03980  4000001        8         System.Int32  1 instance  _myField
                            

Links:

WinDbg links

SOS

SOSEX

HOWTO: Debugging .NET with WinDbg

Article: .NET internals

Book: "Pro .NET Performance" by Sasha Goldshtein, Dima Zurbalev, Ido Flatow

Generics memory layout

An example class:


public class MyGenericClass<T>
{
    private T _myField;

    public T MyMethod()
    {
        return _myField;
    }
}
                        

Compiles to:


.class public auto ansi beforefieldinit
    GenericsUnderTheHood.MyGenericClass`1<T>
        extends [mscorlib]System.Object
{
    .field private !T _myField

    .method public hidebysig
        instance !T MyMethod () cil managed
    {
        ...
    }

    ...
}
                        

Miracle! CLR knows about Generics

An instance:


                            var myObject = new MyGenericClass<object>();
                        

Method table:


0:003> !DumpMT -md 00007fff8e754368
EEClass:         00007fff8e862510
Module:          00007fff8e752fc8
Name:            GenericsUnderTheHood.MyGenericClass`1[[System.Object, mscorlib]]
mdToken:         0000000002000003
File:            C:\Projects\my\GenericsUnderTheHood\GenericsUnderTheHood\bin\Debug\GenericsUnderTheHood.exe
BaseSize:        0x18
ComponentSize:   0x0
Slots in VTable: 6
Number of IFaces in IFaceMap: 0
--------------------------------------
MethodDesc Table
Entry            MethodDesc       JIT    Name
00007fffecc86300 00007fffec8380e8 PreJIT System.Object.ToString()
00007fffeccce760 00007fffec8380f0 PreJIT System.Object.Equals(System.Object)
00007fffeccd1ad0 00007fffec838118 PreJIT System.Object.GetHashCode()
00007fffeccceb50 00007fffec838130 PreJIT System.Object.Finalize()
00007fff8e870210 00007fff8e754280    JIT GenericsUnderTheHood.MyGenericClass`1[[System.__Canon, mscorlib]]..ctor()
00007fff8e75c098 00007fff8e754278   NONE GenericsUnderTheHood.MyGenericClass`1[[System.__Canon, mscorlib]].MyMethod()
                            

EEClass:


0:003> !DumpClass 00007fff8e862510
Class Name:      GenericsUnderTheHood.MyGenericClass`1[[System.__Canon, mscorlib]]
mdToken:         0000000002000003
File:            C:\Projects\my\GenericsUnderTheHood\GenericsUnderTheHood\bin\Debug\GenericsUnderTheHood.exe
Parent Class:    00007fffec824908
Module:          00007fff8e752fc8
Method Table:    00007fff8e7542a0
Vtable Slots:    4
Total Method Slots:  6
Class Attributes:    100001
Transparency:        Critical
NumInstanceFields:   1
NumStaticFields:     0
MT                Field          Offset  Type            VT Attr       Value Name
00007fffecf05c80  4000002        8       System.__Canon  0 instance    _myField
                            

An instance:


                            var myString = new MyGenericClass<string>();
                        

Method table:


0:003> !DumpMT -md 00007fff8e754400
EEClass:         00007fff8e862510
Module:          00007fff8e752fc8
Name:            GenericsUnderTheHood.MyGenericClass`1[[System.String, mscorlib]]
mdToken:         0000000002000003
File:            C:\Projects\my\GenericsUnderTheHood\GenericsUnderTheHood\bin\Debug\GenericsUnderTheHood.exe
BaseSize:        0x18
ComponentSize:   0x0
Slots in VTable: 6
Number of IFaces in IFaceMap: 0
--------------------------------------
MethodDesc Table
Entry       MethodDesc    JIT Name
00007fffecc86300 00007fffec8380e8 PreJIT System.Object.ToString()
00007fffeccce760 00007fffec8380f0 PreJIT System.Object.Equals(System.Object)
00007fffeccd1ad0 00007fffec838118 PreJIT System.Object.GetHashCode()
00007fffeccceb50 00007fffec838130 PreJIT System.Object.Finalize()
00007fff8e870210 00007fff8e754280    JIT GenericsUnderTheHood.MyGenericClass`1[[System.__Canon, mscorlib]]..ctor()
00007fff8e75c098 00007fff8e754278   NONE GenericsUnderTheHood.MyGenericClass`1[[System.__Canon, mscorlib]].MyMethod()
                            

An instance:


                            var myInt = new MyGenericClass<int>();
                        

Method table:


0:003> !DumpMT -md 00007fff8e7544c0
EEClass:         00007fff8e862628
Module:          00007fff8e752fc8
Name:            GenericsUnderTheHood.MyGenericClass`1[[System.Int32, mscorlib]]
mdToken:         0000000002000003
File:            C:\Projects\my\GenericsUnderTheHood\GenericsUnderTheHood\bin\Debug\GenericsUnderTheHood.exe
BaseSize:        0x18
ComponentSize:   0x0
Slots in VTable: 6
Number of IFaces in IFaceMap: 0
--------------------------------------
MethodDesc Table
Entry       MethodDesc    JIT Name
00007fffecc86300 00007fffec8380e8 PreJIT System.Object.ToString()
00007fffeccce760 00007fffec8380f0 PreJIT System.Object.Equals(System.Object)
00007fffeccd1ad0 00007fffec838118 PreJIT System.Object.GetHashCode()
00007fffeccceb50 00007fffec838130 PreJIT System.Object.Finalize()
00007fff8e870260 00007fff8e7544b8    JIT GenericsUnderTheHood.MyGenericClass`1[[System.Int32, mscorlib]]..ctor()
00007fff8e75c0c0 00007fff8e7544b0   NONE GenericsUnderTheHood.MyGenericClass`1[[System.Int32, mscorlib]].MyMethod()
                            

EEClass:


0:003> !DumpClass 00007fff8e862628
Class Name:      GenericsUnderTheHood.MyGenericClass`1[[System.Int32, mscorlib]]
mdToken:         0000000002000003
File:            C:\Projects\my\GenericsUnderTheHood\GenericsUnderTheHood\bin\Debug\GenericsUnderTheHood.exe
Parent Class:    00007fffec824908
Module:          00007fff8e752fc8
Method Table:    00007fff8e7544c0
Vtable Slots:    4
Total Method Slots:  6
Class Attributes:    100001
Transparency:        Critical
NumInstanceFields:   1
NumStaticFields:     0
MT    Field   Offset                 Type VT     Attr            Value Name
00007fffecf03980  4000002        8         System.Int32  1 instance           _myField
                            

Under the hood

awesom-o
  • Value types don't share anything
  • Reference types share code and EEClass
  • System.__Canon - an internal type
  • System.__Canon - will be replaced

Optimizations by CLR

  1. Class loader - 30x
  2. Type hierarchy walk with global dictionary cache lookup - 6x
  3. Global dictionary cache lookup - 3x
  4. Method Table slot - 1x

Note: Optimizes the generic calls in your method not your generic method

Links:

Design and Implementation of Generics for the .NET Common Language Runtime

About generics in CoreCLR documentation

Optimizations explained by a core developer

Dessert

Simplified version:


public class BaseClass<T>
{
    private List<T> _list = new List<T>();

    public BaseClass()
    {
        Enumerable.Empty<T>();
        // or Enumerable.Repeat(new T(), 10);
        // or even new T();
        // or foreach (var item in _list) {}
    }

    public void Run()
    {
        for (var i = 0; i < 8000000; i++)
        {
            if (_list.Any())
            // or if (_list.Count() > 0)
            // or if (_list.FirstOrDefault() != null)
            // or if (_list.SingleOrDefault() != null)
            // or other IEnumerable<T> method
            {
                return;
            }
        }
    }
}

public class DerivedClass : BaseClass<object>
{
}
                            

Benchmark:


public class Program
{
    public static void Main()
    {
        Measure(new DerivedClass());
        Measure(new BaseClass<object>());
    }

    private static void Measure(BaseClass<object>> baseClass)
    {
        var sw = Stopwatch.StartNew();
        baseClass.Run();
        sw.Stop();
        Console.WriteLine(sw.ElapsedMilliseconds);
    }
}
                        

Workaround:

"Just add two methods"


public class BaseClass<T>
{
...
    public void Method1()
    {
    }

    public void Method2()
    {
    }
...
}
                        

Wat?

The fix

The fix The pull request on github

Moral:

Links:

Stackoverflow question

An issue on github

Great explanation of the CLR core developer

The pull request with the fix

Interesting things

Heuristic algorithm


DWORD numMethodsAdjusted =
    (bmtMethod->dwNumDeclaredNonAbstractMethods == 0)
    ? 0
    : (bmtMethod->dwNumDeclaredNonAbstractMethods < 3)
    ? 3
    : bmtMethod->dwNumDeclaredNonAbstractMethods;

DWORD nTypeFactorBy2 = (bmtGenerics->GetNumGenericArgs() == 1)
                       ? 2
                       : 3;

DWORD estNumTypeSlots = (numMethodsAdjusted * nTypeFactorBy2 + 2) / 3;
                            

Sources

Dictionary lookup


CORINFO_GENERIC_HANDLE
JIT_GenericHandleWorker(
    MethodDesc *  pMD,
    MethodTable * pMT,
    LPVOID        signature)
{
     CONTRACTL {
        THROWS;
        GC_TRIGGERS;
    } CONTRACTL_END;

    MethodTable * pDeclaringMT = NULL;

    if (pMT != NULL)
    {
        SigPointer ptr((PCCOR_SIGNATURE)signature);

        ULONG kind; // DictionaryEntryKind
        IfFailThrow(ptr.GetData(&kind));

        // We need to normalize the class passed in (if any) for reliability purposes. That's because preparation of a code region that
        // contains these handle lookups depends on being able to predict exactly which lookups are required (so we can pre-cache the
        // answers and remove any possibility of failure at runtime). This is hard to do if the lookup (in this case the lookup of the
        // dictionary overflow cache) is keyed off the somewhat arbitrary type of the instance on which the call is made (we'd need to
        // prepare for every possible derived type of the type containing the method). So instead we have to locate the exactly
        // instantiated (non-shared) super-type of the class passed in.

        ULONG dictionaryIndex = 0;
        IfFailThrow(ptr.GetData(&dictionaryIndex));

        pDeclaringMT = pMT;
        for (;;)
        {
            MethodTable * pParentMT = pDeclaringMT->GetParentMethodTable();
            if (pParentMT->GetNumDicts() <= dictionaryIndex)
                break;
            pDeclaringMT = pParentMT;
        }

        if (pDeclaringMT != pMT)
        {
            JitGenericHandleCacheKey key((CORINFO_CLASS_HANDLE)pDeclaringMT, NULL, signature);
            HashDatum res;
            if (g_pJitGenericHandleCache->GetValue(&key,&res))
            {
                // Add the denormalized key for faster lookup next time. This is not a critical entry - no need
                // to specify appdomain affinity.
                JitGenericHandleCacheKey denormKey((CORINFO_CLASS_HANDLE)pMT, NULL, signature);
                AddToGenericHandleCache(&denormKey, res);
                return (CORINFO_GENERIC_HANDLE) (DictionaryEntry) res;
            }
        }
    }

    DictionaryEntry * pSlot;
    CORINFO_GENERIC_HANDLE result = (CORINFO_GENERIC_HANDLE)Dictionary::PopulateEntry(pMD, pDeclaringMT, signature, FALSE, &pSlot);

    if (pSlot == NULL)
    {
        // If we've overflowed the dictionary write the result to the cache.
        BaseDomain *pDictDomain = NULL;

        if (pMT != NULL)
        {
            pDictDomain = pDeclaringMT->GetDomain();
        }
        else
        {
            pDictDomain = pMD->GetDomain();
        }

        // Add the normalized key (pDeclaringMT) here so that future lookups of any
        // inherited types are faster next time rather than just just for this specific pMT.
        JitGenericHandleCacheKey key((CORINFO_CLASS_HANDLE)pDeclaringMT, (CORINFO_METHOD_HANDLE)pMD, signature, pDictDomain);
        AddToGenericHandleCache(&key, (HashDatum)result);
    }

    return result;
}
                                

Sources

TODO

Generic method:


public class MyClassWithGenericMethod
{
    public T MyGenericMethod<T>(T arg)
    {
        return arg;
    }
}
                        

Generic struct:


public struct MyGenericStruct<T>
{
    private T _myField;

    public T MyMethod()
    {
        return _myField;
    }
}
                        

THE END