📜 ⬆️ ⬇️

Different versions of JIT in .NET

Every C # developer is aware that the C # compiler translates the program source code into an intermediate language called Intermediate Language (IL). And for turning IL into a sequence of machine commands, the Just-In-Time compiler (JIT) is most often responsible. Yes, today there is NGen, Mono AOT, .NET Native, but JIT compilation still leads the world of .NET applications. But this is the very JIT, they know not all. If we take into account only the implementation of .NET from Microsoft, then it is worth distinguishing between JIT-x86 and JIT-x64. And behind the door stands RyuJIT which very soon will take the honorable place of the main JIT compiler. And if you like the old versions of .NET, then it is useful to know that the JIT logic was different in different versions of the CLR. Our sources are now open, you can see them and realize how much this is a big and complex topic. Today we will not try to cover it, but only briefly look at a few interesting features of the individual versions of the JIT compilers. So, today in the room:


JIT-x86 and starg


Open the Decimal constructor source with an int parameter from the .NET Reference Source:

 // Constructs a Decimal from an integer value. // public Decimal(int value) { // JIT today can't inline methods that contains "starg" opcode. // For more details, see DevDiv Bugs 81184: x86 JIT CQ: Removing the inline striction of "starg". int value_copy = value; if (value_copy >= 0) { flags = 0; } else { flags = SignMask; value_copy = -value_copy; } lo = value_copy; mid = 0; hi = 0; } 

Intrigued? And the thing is that JIT-x86 does not know how to inline methods whose IL-code contains instructions starg or ldarga . The decimal constructor is very desirable to be inlined, so the developers of the standard class went to the trick: they copied the parameter to a local variable to avoid the “bad” instruction. In JIT-x64, this feature was removed. For those interested, it is recommended to study:

Strange bug in JIT-x64


Dear experts, attention, question: what will the following code for step=1 ?
')
 private int bar; public void Foo(int step) { for (int i = 0; i < step; i++) { bar = i + 10; for (int j = 0; j < 2 * step; j += step) Console.WriteLine(j + 10); } } 

The correct answer: depends. Most likely you expect to see 10 11 , but a bug in optimizing JIT-x64 will ruin everything and give us 10 21 . In JIT-x86 and RyuJIT, everything works well. With the bug will have to accept, Microsoft does not want to fix it. The example is very fragile, stumble upon it in real life is extremely problematic. Someone will ask: but if this is a rare bug, then why know about it? Why bother with such things at all? If you are a funny person, you can use the bug for your own purposes. For example, determine which version of JIT is currently used in runtime:

 public enum JitVersion { Mono, MsX86, MsX64, RyuJit } public class JitVersionInfo { public JitVersion GetJitVersion() { if (IsMono()) return JitVersion.Mono; if (IsMsX86()) return JitVersion.MsX86; if (IsMsX64()) return JitVersion.MsX64; return JitVersion.RyuJit; } private int bar; private bool IsMsX64(int step = 1) { var value = 0; for (int i = 0; i < step; i++) { bar = i + 10; for (int j = 0; j < 2 * step; j += step) value = j + 10; } return value == 20 + step; } public static bool IsMono() { return Type.GetType("Mono.Runtime") != null; } public static bool IsMsX86() { return !IsMono() && IntPtr.Size == 4; } } 

Additional reading material:


Unwinding cycles


Unwinding cycles is such a very good optimization that many compilers love to do. The bottom line is that we replace the view loop.

 for (int i = 0; i < 1024; i++) Foo(i); 

on

 for (int i = 0; i < 1024; i += 4) { Foo(i); Foo(i + 1); Foo(i + 2); Foo(i + 3); } 

In addition to reducing the number of increment operations, we have improved conditions for additional operations at the processor level (for example, branch prediction and instruction-level parallelism). Alas, JIT-x86 and RyuJIT do not really know how to unwind the average cycle. But JIT-x64 can sometimes, although it does so in its own special way. For example, if the number of iterations is divisible by 2 or 3, then the code

 int sum = 0; for (int i = 0; i < 1024; i++) sum += i; Console.WriteLine(sum); 

turn into something of a kind

 ; int sum = 0; 00007FFCC8710090 sub rsp,28h ; for (int i = 0; i < 1024; i++) 00007FFCC8710094 xor ecx,ecx 00007FFCC8710096 mov edx,1 ; edx = i + 1 00007FFCC871009B nop dword ptr [rax+rax] 00007FFCC87100A0 lea eax,[rdx-1] ; eax = i ; sum += i; 00007FFCC87100A3 add ecx,eax ; sum += i 00007FFCC87100A5 add ecx,edx ; sum += i + 1 00007FFCC87100A7 lea eax,[rdx+1] ; eax = i + 2 00007FFCC87100AA add ecx,eax ; sum += i + 2; 00007FFCC87100AC lea eax,[rdx+2] ; eax = i + 3 00007FFCC87100AF add ecx,eax ; sum += i + 3; 00007FFCC87100B1 add edx,4 ; i += 4 ; for (int i = 0; i < 1024; i++) 00007FFCC87100B4 cmp edx,401h 00007FFCC87100BA jl 00007FFCC87100A0 

This is quite important information. For example, many are looking forward to switching from JIT-x64 to RyuJIT, because Microsoft promises us a lot of tasty things: SIMD support and accelerated JIT compilation. But about the performance of the code itself, they are somehow silent. It should be understood that the lack of some optimizations in RyuJIT (compared to JIT-x64) can slightly reduce the speed of your program. Useful links:

More interesting JIT bugs


Here you have another problem:

 struct Point { public int X; public int Y; } static void Print(Point p) { Console.WriteLine(pX + " " + pY); } static void Main() { var p = new Point(); for (pX = 0; pX < 2; p.X++) Print(p); } 

This cycle can also be unwound. There are only two iterations, so you can get rid of the conditional transitions altogether: it is enough to repeat the body of the loop twice. An interesting fact: in the CLR2 JIT-x86 there was a bug that spoiled life and instead of 0 1 1 0 produced 2 0 2 0 . Stumble upon it is not so difficult. Fortunately, in the CLR 4 it was corrected, and in other versions of JIT it was not at all. Keep in mind that if you are working under .NET Framework 3.5 (yes, some still have to), this implies CLR2. You need to be prepared that such a simple code will turn into

 ; var p = new Point(); 05C5178C push esi 05C5178D xor esi,esi ; pY = 0 ; for (pX = 0; pX < 2; p.X++) 05C5178F lea edi,[esi+2] ; pX = 2 ; Print(p); 05C51792 push esi ; push pY 05C51793 push edi ; push pX 05C51794 call dword ptr ds:[54607F4h] ; Print(p) 05C5179A push esi ; push pY 05C5179B push edi ; push pX 05C5179C call dword ptr ds:[54607F4h] ; Print(p) 05C517A2 pop esi 05C517A3 pop edi 05C517A4 pop ebp 05C517A5 ret 

In general, the topic of unwinding small cycles is of particular interest. While JIT-x86 likes to unwind them (this is a big cycle, unwinding is difficult, but with a little everything is much easier), RyuJIT (which is based on the 32-bit JIT codebase) refuses to unwind them. But JIT-x64 here we can please. Let's say he can take the code

 int sum = 0; for (int i = 0; i < 4; i++) sum += i; Console.WriteLine(sum); 

and prefetch the value:

 ; int sum = 0; 00007FFCC86F3EC0 sub rsp,28h ; Console.WriteLine(sum); 00007FFCC86F3EC4 mov ecx,6 ; sum = 6 00007FFCC86F3EC9 call 00007FFD273DCF10 00007FFCC86F3ECE nop 00007FFCC86F3ECF add rsp,28h 00007FFCC86F3ED3 ret 

But do not think that RyuJIT is worse at all than JIT-x64. Yes, with optimizations in the JIT compiler of the new generation, everything is not so good, but on average, the code in the hospital is more sane. You can learn more about unwinding small cycles here:

Want to know more about .NET internals?


Then come to us at the light! In a short time in Moscow (April 03–04), Yekaterinburg (May 17) and St. Petersburg (May 29–30) a series of CLRium # 2 seminars will be held (live streaming is on). We will discuss the future of .NET: let's talk about the anatomy of the new CoreCLR, features of RyuJIT, hardcore examples of working with Roslyn and CoreFx giblets! An endless stream of interesting and useful knowledge will help you not only understand better how your own C # programs work, but also prepare you for a brighter .NET future in which you can use the power of the platform to the fullest!

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


All Articles