Age of JIT compiling. Part II. CLR is watching you
Продолжая тему JIT-компиляции .NET’a, сегодня мы рассмотрим диспетчеризацию методов у интерфейсов, generics (как классов, так и отдельных методов вместе с реальными сигнатурами); производить отладку релизных сборок с оптимизациями; разберемся с истинным предназначением типа System.__Canon (это не то, что Вы подумали).Настройка средыПрежде чем двигаться дальше, нам необходимо подготовить Visual Studio для отладки релизных сборок.Использовать будем VS 2013, поэтому для использования SOS.dll придется включить compatibility mode:
Tools → Options → Debugging → General Далее снимем галочки здесь же с: Suppress JIT optimization on module load Enable Just My Code Также необходимо включить поддержку Native Debugging: Project Settings → Debug → Enable native code debuggingТеперь приступим к нашим исследованиям.Interface dispatch stubs (Virtual Stub Dispatch) CLR постоянно проводит мониторинг всех участков кода. Имеет несколько стратегий по обновлению нативного кода методов. Именно так — не только HotSpot в Java имеет такой функционал, или же современные JS-движки.Такой функционал появился в CLR 2.0 еще в 2006 году. И…остался во многом в таком же виде + новые эвристики.
Особенно «бдительно» среда следит за интерфейсами.Надеюсь, Вы уже настроили студию для дебага релизного кода.
Рассмотрим пример: class Program { static void Main (string[] args) { ICallable target = new FirstCallableImpl (); CallInterface (target);
ICallable target2 = new SecondCallableImpl (); CallInterface (target2); }
[MethodImpl (MethodImplOptions.NoInlining)] private static void CallInterface (ICallable callable) { for (int i = 0; i < 1000000; i++) { callable.DoSomething(); // place breakpoint } } }
interface ICallable { void DoSomething (); }
class FirstCallableImpl: ICallable { public void DoSomething () {
} }
class SecondCallableImpl: ICallable { public void DoSomething () {
} } Запустим отладку. Далее откроем окно Disassembly (Debug → Windows → Disassembly).Рассмотрим инструкцию call dword ptr ds:[00450010h].Чтобы узнать значение по адресу 0×00450010 откроем окно памяти (Debug → Windows→ Memory→ Memory1).
На данном этапе JIT еще не создал необходимый узел вызова, пока что среда сама производит «интерпретацию» вызова метода интерфейса (это значит, происходит линейный поиск требуемого метода в рантайме).
Однако позволим еще 2 раза выполниться этому коду и увидим, что значение адреса 0×0450010 изменилось:
Для инспекции значения 00457012 загрузим SOS.dll: Immediate window → .load sos
! u 00457012 Unmanaged code 00457012 813908314400 cmp dword ptr [ecx],443108h 00457018 0F85F32F0000 jne 0045A011 0045701E E9BD901D00 jmp 006300E0 Инструкция jmp 006300E0 представляет собой вызов требуемого метода интерфейса. Проверим: ! u 006300E0 Normal JIT generated code ConsoleApplication1.FirstCallableImpl.DoSomething () Begin 006300e0, size 1 >>> 006300E0 C3 ret Так… С методом понятно, но что же за сравнение происходит в инструкции cmp dword ptr [ecx],443108h? ! DumpMT 443108 EEClass: 00441378 Module: 00442c5c Name: ConsoleApplication1.FirstCallableImpl mdToken: 02000004 (C:\*path to project*\InterfaceStubsTest.exe) BaseSize: 0xc ComponentSize: 0×0 Number of IFaces in IFaceMap: 1 Slots in VTable: 6 Ага! Сравниваем this на соответствие типу FirstCallableImpl (т.е. MethodTable) и при значении true вызываем метод FirstCallableImpl.DoSomething ().Инструкция jne 0045A011 представляет собой fallback на линейный поиск, как и было до кэширования.Когда дело дойдет до вызова следующего типа — SecondCallableImpl, то все так же будет проверяться в узле вызова именно FirstCallableImpl, а не SecondCallableImpl.
Но это же неэффективно! Именно поэтому, по достижению определенного количества итераций вызова кода, среда просто заменит данный узел вызова с кэшем на (как Вы уже догадались) линейный поиск.
Кэширование весьма эффективно, если мы вызываем методы у коллекций, например.
Generic types stubs Выход CLR 2.0 вместе с generics ознаменовал существенные изменения в среде исполнения. Если до этого для описания конкретного типа «хватало» лишь структуры EEClass, то теперь связка структура EEClass+MethodTable представляет собой текущий тип.Более того, для List<string> и List<int> разными будут даже EEClass (про code-sharing будет чуть ниже).
Рассмотрим пример: class Program { static void Main (string[] args) { var refTypeHolder = new HolderOf
// call JIT refTypeHolder.GetPointer (); intTypeHolder.GetPointer ();
Console.Read (); // place breakpoint } }
class HolderOf
public HolderOf (T pointer) { _pointer = pointer; }
public T GetPointer () { return _pointer; } } Для инспекции используем команду ! dumpheap: .load sos.dll
! dumpheap -type HolderOf
PDB symbol for mscorwks.dll not loaded
Address MT Size
02d332c8 00f531e0 12
02d332d4 00f53268 12
total 2 objects
Statistics:
MT Count TotalSize Class Name
00f53268 1 12 ConsoleApplication1.HolderOf`1[[System.Int32, mscorlib]]
00f531e0 1 12 ConsoleApplication1.HolderOf`1[[System.Object, mscorlib]]
Total 2 objects
Как мы видим, среда создала две различные специализации класса HolderOf
! dumpmd 00f5325c (HolderOf
[Serializable ()] [ClassInterface (ClassInterfaceType.AutoDual)] [ComVisible (true)] internal class __Canon { } Если кратко, то обычно говорят, что для ссылочных типов среда использует тип System.__Canon для шаринга кода.Но не в этом дело. Серьезно.
Дело в том, что generic-типы могут содержать циклические зависимости от других типов, что чревато бесконечным созданием специализаций для кода. Например: Generics cyclomatic dependencies
class GenericClassOne
class GenericClassTwo
{
private GenericClassThree
class GenericClassThree
{
private GenericClassTwo
Type loader (он же загрузчик типов) сканирует каждый generic-тип на наличие циклической зависимости и присваивает очередность (т.н. LoadLevel для класса). Хотя все специализации для ref-types имеют System.__Canon как аргумент типа — это следствие, а не причина.
Фазы загрузки (они же ClassLoadLevel):
enum ClassLoadLevel { CLASS_LOAD_BEGIN, CLASS_LOAD_UNRESTOREDTYPEKEY, CLASS_LOAD_UNRESTORED, CLASS_LOAD_APPROXPARENTS, CLASS_LOAD_EXACTPARENTS, CLASS_DEPENDENCIES_LOADED, CLASS_LOADED, CLASS_LOAD_LEVEL_FINAL = CLASS_LOADED, }; Для SSLCI (Rotor) код, ответственный за сканирование находится в файле sscli20/clr/src/vm/Generics.cpp: Generics.cpp BOOL Generics: CheckInstantiationForRecursion (const unsigned int nGenericClassArgs, const TypeHandle pGenericArgs[]) { CONTRACTL { NOTHROW; GC_NOTRIGGER; } CONTRACTL_END; if (nGenericClassArgs == 0) return TRUE;
_ASSERTE (pGenericArgs);
struct PerIterationData { const TypeHandle * genArgs; int index; int numGenArgs; }; PerIterationData stack[MAX_GENERIC_INSTANTIATION_DEPTH]; stack[0].genArgs = pGenericArgs; stack[0].numGenArgs = nGenericClassArgs; stack[0].index = 0; int curDepth = 0;
// Walk over each instantiation, doing a depth-first search looking for any // instantiation with a depth of over 100, in an attempt at flagging // recursive type definitions. We’re doing this to help avoid a stack // overflow in the loader. // Avoid recursion here, to avoid a stack overflow. Also, this code // doesn’t allocate memory. while (curDepth >= 0) { PerIterationData * cur = &stack[curDepth]; if (cur→index == cur→numGenArgs) { // Pop curDepth--; if (curDepth >= 0) stack[curDepth].index++; continue; } if (cur→genArgs[cur→index].HasInstantiation ()) { // Push curDepth++; if (curDepth >= MAX_GENERIC_INSTANTIATION_DEPTH) return FALSE; stack[curDepth].genArgs = cur→genArgs[cur→index].GetInstantiation (); stack[curDepth].numGenArgs = cur→genArgs[cur→index].GetNumGenericArgs (); stack[curDepth].index = 0; continue; } // Continue to the next item cur→index++; } return TRUE; } Для CoreCLR код изменился в сторону ООП :)Итак, разобрались: ссылочные типы имеют шаринг кода, значимые — нет… А почему? Если все сводится к размеру типа (ref — размер слова; In32 — 4 байта, double — 8 байт и т.д.), тогда можно для DateTime и long расшарить.
Во-первых, это неправильно с точки зрения семантики. Во-вторых, разработчики CLR решили этого не делать.
Generic method stubs Мы рассмотрели специализацию кода для generic-типов, а как насчет методов? Как найти отдельные методы вне класса? Рассмотрим пример: Generic methods class Program { static void Main (string[] args) { var refTypeHolder = new HolderOf (); Test (refTypeHolder); Test2(refTypeHolder); Console.Read (); }
[MethodImpl (MethodImplOptions.NoInlining)]
static void Test (HolderOf typeHolder)
{
for (int i = 0; i < 10; i++)
{
typeHolder.GetPointer
[MethodImpl (MethodImplOptions.NoInlining)] static void Test2(HolderOf typeHolder) { for (int i = 0; i < 10; i++) { typeHolder.GetPointer
class HolderOf
{
[MethodImpl (MethodImplOptions.NoInlining)]
public void GetPointer
Исследуем:! dumpmd 10031B8 (from Test ()) Method Name: ConsoleApplication1.HolderOf.GetPointer[[ConsoleApplication1.Program, InterfaceStubsTest]]() Class: 01001444 MethodTable: 01003118 mdToken: 0600000e Module: 01002c5c IsJitted: no CodeAddr: ffffffffffffffff ! dumpmd 1003574 (from Test2()) ! dumpmd 1003574Method Name: ConsoleApplication1.HolderOf.GetPointer[[System.Object, mscorlib]]()Class: 01001444MethodTable: 01003118mdToken: 0600000eModule: 01002c5cIsJitted: noCodeAddr: ffffffffffffffff
Ага! передается структура MethodDesc, которая содержит в себе указатель на MethodTable (хочу заметить — оба дескриптора указывают на один и тот же MethodTable 0×01003118) и служит источником метаданных.Таким образом, при вызове generic-методов, передается дополнительный параметр с MethodDesc.Сами адреса FFE8BF40 и FFE8BE40 являются трамплином, который отдает (forward) реальный специализированный (для int, object и т.д.) нативный код.
Т.к. сам дескриптор также хранит в себе generic-параметры, то получается еще и экономия на количестве передаваемых аргументов в случае, например, нескольких generic-параметров Some<T, TU, TResult>().