Настройка среды
Прежде чем двигаться дальше, нам необходимо подготовить Visual Studio для отладки релизных сборок.
Использовать будем VS 2013, поэтому для использования SOS.dll придется включить compatibility mode:
Далее снимем галочки здесь же с:
- 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]
.
Чтобы узнать значение по адресу 0x00450010 откроем окно памяти (Debug -> Windows-> Memory-> Memory1).
На данном этапе JIT еще не создал необходимый узел вызова, пока что среда сама производит «интерпретацию» вызова метода интерфейса (это значит, происходит линейный поиск требуемого метода в рантайме).
Однако позволим еще 2 раза выполниться этому коду и увидим, что значение адреса 0x0450010 изменилось:
Для инспекции значения 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: 0x0
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<object>(null);
var intTypeHolder = new HolderOf<int>(0);
// call JIT
refTypeHolder.GetPointer();
intTypeHolder.GetPointer();
Console.Read(); // place breakpoint
}
}
class HolderOf<T>
{
private readonly T _pointer;
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<T>
!dumpmt -md 00f53268
EEClass: 00f514cc
Module: 00f52c5c
Name: ConsoleApplication1.HolderOf`1[[System.Int32, mscorlib]]
mdToken: 02000006 (C:\*path to samples*\InterfaceStubsTest.exe)
BaseSize: 0xc
ComponentSize: 0x0
Number of IFaces in IFaceMap: 0
Slots in VTable: 6
--------------------------------------
MethodDesc Table
Entry MethodDesc JIT Name
66ae6a30 66964968 PreJIT System.Object.ToString()
66ae6a50 66964970 PreJIT System.Object.Equals(System.Object)
66ae6ac0 669649a0 PreJIT System.Object.GetHashCode()
66b57940 669649c4 PreJIT System.Object.Finalize()
00f5c088 00f53250 JIT ConsoleApplication1.HolderOf`1[[System.Int32, mscorlib]]..ctor(Int32)
00f5c090 00f5325c NONE ConsoleApplication1.HolderOf`1[[System.Int32, mscorlib]].GetPointer()
!dumpmt -md 00f531e0
EEClass: 00f51438
Module: 00f52c5c
Name: ConsoleApplication1.HolderOf`1[[System.Object, mscorlib]]
mdToken: 02000006 (C:\*path to samples*\InterfaceStubsTest.exe)
BaseSize: 0xc
ComponentSize: 0x0
Number of IFaces in IFaceMap: 0
Slots in VTable: 6
--------------------------------------
MethodDesc Table
Entry MethodDesc JIT Name
66ae6a30 66964968 PreJIT System.Object.ToString()
66ae6a50 66964970 PreJIT System.Object.Equals(System.Object)
66ae6ac0 669649a0 PreJIT System.Object.GetHashCode()
66b57940 669649c4 PreJIT System.Object.Finalize()
00f5c068 00f53154 JIT ConsoleApplication1.HolderOf`1[[System.__Canon, mscorlib]]..ctor(System.__Canon)
00f5c070 00f53160 NONE ConsoleApplication1.HolderOf`1[[System.__Canon, mscorlib]].GetPointer()
В вышеприведенном дампе, нас интересует HolderOf<T>.GetPointer()
. Рассмотрим:
!dumpmd 00f5325c
Method Name: ConsoleApplication1.HolderOf`1[[System.Int32, mscorlib]].GetPointer()
Class: 00f514cc
MethodTable: 00f53268
mdToken: 0600000b
Module: 00f52c5c
IsJitted: yes
CodeAddr: 01090318
!dumpmd 00f53160
Method Name: ConsoleApplication1.HolderOf`1[[System.__Canon, mscorlib]].GetPointer()
Class: 00f51438
MethodTable: 00f53178
mdToken: 0600000b
Module: 00f52c5c
IsJitted: yes
CodeAddr: 010902b8
HolderOf<object> | HolderOf<int> | |
---|---|---|
MethodDesc | 00f53160 | 00f5325c |
CodeAddr | 01090318 | 010902b8 |
Initiation type | HolderOf`1[[System.__Canon, mscorlib]] | HolderOf`1[[System.Int32, mscorlib]] |
Итак, мы видим, что отличаются не только Methodtable, но и нативный код (CodeAddr).
А теперь самое интересное – куда делся System.Object для Holderof<object> ?! Что за System.__Canon?
Знакомьтесь:
[Serializable()]
[ClassInterface(ClassInterfaceType.AutoDual)]
[ComVisible(true)]
internal class __Canon
{
}
Если кратко, то обычно говорят, что для ссылочных типов среда использует тип System.__Canon для шаринга кода.
Но не в этом дело. Серьезно.
Дело в том, что generic-типы могут содержать циклические зависимости от других типов, что чревато бесконечным созданием специализаций для кода. Например:
class GenericClassOne<T>
{
private T field;
}
class GenericClassTwo<U>
{
private GenericClassThree<GenericClassOne<U>> field
}
class GenericClassThree<S>
{
private GenericClassTwo<GenericClassOne<S>> field
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine((new GenericClassTwo<object>()).ToString());
Console.Read();
}
}
Однако этот код не упадет и выведет GenericClassTwo`1[System.Object].
— Так и что там про зависимости было? (примечание: мысли вслух).
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
: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-типов, а как насчет методов? Как найти отдельные методы вне класса?
Рассмотрим пример:
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<Program>();
}
} // place breakpoint
[MethodImpl(MethodImplOptions.NoInlining)]
static void Test2(HolderOf typeHolder)
{
for (int i = 0; i < 10; i++)
{
typeHolder.GetPointer<object>();
}
} // place breakpoint
}
class HolderOf
{
[MethodImpl(MethodImplOptions.NoInlining)]
public void GetPointer<T>()
{
Console.WriteLine(typeof(T));
}
}
В точке останова в окне Disassembly для метода Test() можно увидеть следующее:
00000045 mov ecx,dword ptr [ebp-3Ch]
00000048 mov edx,10031B8h
0000004d cmp dword ptr [ecx],ecx
0000004f call FFE8BF40
А для Test2() — следующее:
00000045 mov ecx,dword ptr [ebp-3Ch]
00000048 mov edx,1003574h
0000004d cmp dword ptr [ecx],ecx
0000004f call FFE8BE40
Регистр ECX содержит указатель на this (calling convention — FastCall), но ведь GetPointer() имеет ноль аргументов, что же тогда записывается в EDX?!
Исследуем:
Method Name: ConsoleApplication1.HolderOf.GetPointer[[ConsoleApplication1.Program, InterfaceStubsTest]]()
Class: 01001444
MethodTable: 01003118
mdToken: 0600000e
Module: 01002c5c
IsJitted: no
CodeAddr: ffffffffffffffff
!dumpmd 1003574
Method Name: ConsoleApplication1.HolderOf.GetPointer[[System.Object, mscorlib]]()
Class: 01001444
MethodTable: 01003118
mdToken: 0600000e
Module: 01002c5c
IsJitted: no
CodeAddr: ffffffffffffffff
Ага! передается структура MethodDesc, которая содержит в себе указатель на MethodTable (хочу заметить — оба дескриптора указывают на один и тот же MethodTable 0x01003118) и служит источником метаданных.
Таким образом, при вызове generic-методов, передается дополнительный параметр с MethodDesc.
Сами адреса FFE8BF40 и FFE8BE40 являются трамплином, который отдает (forward) реальный специализированный (для int, object и т.д.) нативный код.
Т.к. сам дескриптор также хранит в себе generic-параметры, то получается еще и экономия на количестве передаваемых аргументов в случае, например, нескольких generic-параметров Some<T, TU, TResult>().
This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.
Комментариев нет:
Отправить комментарий