📜 ⬆️ ⬇️

Age of JIT compiling. Part II. CLR is watching you

Continuing the theme of the .NET JIT compilation , today we will look at the dispatching of methods around interfaces, generics (both classes and individual methods, along with real signatures); debug release releases with optimizations; let's deal with the true purpose of the System .__ Canon type (this is not what you thought).

Environment setup


Before proceeding further, we need to prepare Visual Studio for debugging release builds.

We’ll use VS 2013, so to use SOS.dll you’ll have to enable compatibility mode:
Tools -> Options -> Debugging -> General


Next, remove the checkboxes here with:

You also need to enable Native Debugging support:
Project Settings -> Debug -> Enable native code debugging
Now we proceed to our research.

Interface dispatch stubs (Virtual Stub Dispatch)


The CLR constantly monitors all parts of the code. It has several strategies for updating native method code. Exactly - not only HotSpot in Java has such functionality, or modern JS engines.
')
This functionality appeared in CLR 2.0 back in 2006. And ... remained in much the same way + new heuristics.

Especially the “vigilant” environment monitors the interfaces.
I hope you have already set up a studio for debug release code.
Consider an example:
 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() { } } 


Let's start debugging. Next, open the Disassembly window (Debug -> Windows -> Disassembly).


Consider the instruction call dword ptr ds:[00450010h] .
To find out the value at 0x00450010, open the memory window (Debug -> Windows-> Memory-> Memory1).


At this stage, the JIT has not yet created the necessary call node, so far the environment itself performs an “interpretation” of calling the interface method (this means, a linear search for the required method occurs in runtime).

However, let's allow this code to be executed 2 more times and see that the value of the address 0x0450010 has changed:


To inspect the value 00457012, load the 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 

The jmp 006300E0 is a call to the required interface method. Check:
 !u 006300E0 Normal JIT generated code ConsoleApplication1.FirstCallableImpl.DoSomething() Begin 006300e0, size 1 >>> 006300E0 C3 ret 

So ... With a method it is clear, but what for comparison happens in the instruction 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 

Aha We compare this to a type matching FirstCallableImpl (i.e. MethodTable) and, if true, call FirstCallableImpl.DoSomething ().
The jne 0045A011 is a fallback to a linear search, as it was before caching.

When it comes to calling the following type - SecondCallableImpl , it will still be checked in the call node of FirstCallableImpl, and not SecondCallableImpl.

But this is ineffective! That is why, upon reaching a certain number of code call iterations, the environment will simply replace the given call node with the cache with (you guessed it) linear search.
Caching is very efficient if we call methods on collections, for example.

Generic types stubs


The release of CLR 2.0 along with generics marked significant changes in the execution environment. If before this only the EEClass structure was enough to describe a specific type, now the bundle EEClass + MethodTable structure is the current type.

Moreover, for List <string> and List <int>, even EEClass will be different (about code-sharing will be slightly lower).
Consider an example:
 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; } } 


For inspection, use the command !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 

As we can see, the environment has created two different specializations of the HolderOf<T> class
! dumpmt -md 00f53268 (HolderOf <int>)
 !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 (HolderOf <object>)
 !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() 



In the above dump, we are interested in HolderOf<T>.GetPointer() . Consider:
! dumpmd 00f5325c (HolderOf <int> .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 (HolderOf <object> .GetPointer ())
 !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>
MethodDesc00f5316000f5325c
CodeAddr01090318010902b8
Initiation typeHolderOf`1 [[System .__ Canon, mscorlib]]HolderOf`1 [[System.Int32, mscorlib]]

So, we see that not only Methodtable differs, but also native code (CodeAddr).

And now the most interesting - where did System.Object go for Holderof <object> ?! What is the System .__ Canon ?
Meet:
 [Serializable()] [ClassInterface(ClassInterfaceType.AutoDual)] [ComVisible(true)] internal class __Canon { } 

In short, it is usually said that for reference types the medium uses the Canon System .__ type to share the code.
But that's not the point. Seriously.

The fact is that generic types can contain cyclical dependencies on other types, which is fraught with the endless creation of specializations for code. For example:
Generics cyclomatic dependencies
 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(); } } 


However, this code will not fall and will output GenericClassTwo`1 [System.Object] .

- So what was there about addiction? (note: thoughts out loud).

A type loader (also known as a type loader) scans each generic type for the presence of a circular dependency and assigns a priority (the so-called LoadLevel for a class). Although all specializations for ref-types have System .__ Canon as a type argument - this is a consequence, not a reason.

Loading phases (they are also 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, }; 

For SSLCI (Rotor), the code responsible for scanning is in the sscli20/clr/src/vm/Generics.cpp file:
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; } 


For CoreCLR, the code has changed towards OOP :)

So, you figured out: reference types have a sharing code, meaningful ones don't ... And why?
If it all comes down to the size of a type (ref is the word size; In32 is 4 bytes, double is 8 bytes, etc.), then it is possible to share it for DateTime and long.

First, it is wrong in terms of semantics. Second, the CLR developers decided not to.

Generic method stubs


We looked at code specialization for generic types, but what about the methods? How to find individual methods outside the class?
Consider an example:
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<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)); } } 


At the breakpoint in the Disassembly window for the Test () method, you can see the following:
 00000045 mov ecx,dword ptr [ebp-3Ch] 00000048 mov edx,10031B8h 0000004d cmp dword ptr [ecx],ecx 0000004f call FFE8BF40 

And for Test2 () - the following:
 00000045 mov ecx,dword ptr [ebp-3Ch] 00000048 mov edx,1003574h 0000004d cmp dword ptr [ecx],ecx 0000004f call FFE8BE40 

The ECX register contains a pointer to this (calling convention - FastCall), but after all, GetPointer () has zero arguments, what then is written to the EDX ?!

We investigate:
! dumpmd 10031B8 (from Test ())
 !dumpmd 10031B8 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 1003574 Method Name: ConsoleApplication1.HolderOf.GetPointer[[System.Object, mscorlib]]() Class: 01001444 MethodTable: 01003118 mdToken: 0600000e Module: 01002c5c IsJitted: no CodeAddr: ffffffffffffffff 


Aha The MethodDesc structure is passed , which contains a pointer to MethodTable (I want to note that both descriptors point to the same MethodTable 0x01003118 ) and serves as a source of metadata.

Thus, when calling generic methods, an additional parameter is passed with MethodDesc.
The addresses FFE8BF40 and FFE8BE40 are the springboard, which gives (forward) the real specialized (for int, object, etc.) native code.

Since the descriptor itself also stores generic parameters in itself; this also results in saving on the number of arguments passed in the case of, for example, several generic parameters Some <T, TU, TResult> () .

Source: https://habr.com/ru/post/253105/


All Articles