.NETのJITの異なるバージョン

すべてのC#開発者は、C#コンパイラがアプリケーションのソースコードをIntermediate Language(IL)と呼ばれる中間言語に翻訳することを知っています。 また、ILを一連の機械命令に変換するために、Just-In-Time Compiler(JIT)が最も頻繁に責任を負います。 はい、今日はNGen、Mono AOT、.NET Nativeがありますが、JITコンパイルは依然として.NETアプリケーションの世界をリードしています。 しかし、これと同じJITが機能し、誰もが知っているわけではありません。 Microsoftの.NET実装のみを考慮する場合、JIT-x86とJIT-x64を区別する価値があります。 そして、その背後にはRyuJITがあり、これはまもなくメインのJITコンパイラーの名誉を引き継ぐことになります。 古いバージョンの.NETが好きなら、CLRの異なるバージョンではJITのロジックが異なることを知っておくと便利です。 ソースコードが開いたので、それらを見て、このトピックがどれほど大きく複雑であるかを理解できます 。 今日はそれをカバーしようとはせず、JITコンパイラーの個々のバージョンのいくつかの興味深い機能を簡単に見ていきます。 だから今日の問題で:





JIT-x86およびstarg



.NET Reference Sourceのint



パラメーターを使用してDecimal



コンストラクターのソースを開きます。



 // 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; }
      
      





興味がありますか? そして、問題は、JIT-x86がILコードにstarg



またはldarga



含むメソッドをインライン化できないことです。 Decimalコンストラクターをインライン化することが非常に望ましいため、標準クラスの開発者は、「悪い」命令を避けるためにパラメーターをローカル変数にコピーしました。 JIT-x64では、この「機能」は削除されました。 興味のある方は、以下を勉強することをお勧めします。

JIT-x64の奇妙なバグ



親愛なる専門家、注意、質問: 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); } }
      
      





正しい答え:依存します。 ほとんどの場合、 10 11



を見ることを期待しますが、JIT-x64最適化のバグはすべてを台無しにし、 10 21



を与えます。 JIT-x86およびRyuJITでは、すべてがうまく機能します。 バグに我慢する必要があります;マイクロソフトはそれを修正したくありません。 この例は非常に壊れやすく、実際の生活でつまずくのは非常に問題です。 誰かが尋ねます:しかし、これがまれなバグであるならば、なぜそれについて知っていますか? なぜそんなことに興味があるのですか? あなたが元気な人なら、バグを自分の目的に使うことができます。 たとえば、ランタイムで現在使用されているJITのバージョンを確認するには:



 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; } }
      
      





追加資料:





巻き戻しサイクル



ループの巻き戻しは非常に優れた最適化であり、多くのコンパイラーが好んでいます。 一番下の行は、フォームのループを置き換えることです



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









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





インクリメント操作の数を減らすことに加えて、プロセッサレベルでの追加操作の条件を改善しました(たとえば、分岐予測や命令レベルの並列処理)。 残念ながら、JIT-x86とRyuJITは平均的なサイクルを特に解くことができません。 ただし、JIT-x64は、独自の特別な方法で実行しますが、場合によっては実行できます。 たとえば、反復回数が2または3で除算される場合、コードは



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





一種の何かに変わります



 ; 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
      
      





これは非常に重要な情報です。 たとえば、多くの人はJIT-x64からRyuJITへの切り替えを楽しみにしています。なぜなら、MicrosoftはSIMDサポートとJITコンパイルの高速化という多くの利点を約束しているからです。 しかし、彼らはコード自体のパフォーマンスについては何とか沈黙しています。 (JIT-x64と比較して)RyuJITにいくつかの最適化がないため、プログラムの速度がわずかに低下する可能性があることを理解する必要があります。 便利なリンク:

より興味深いJITバグ



ここに別のパズルがあります:



 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); }
      
      





このサイクルはねじれを解くこともできます。 反復は2回だけなので、条件付き遷移を完全に取り除くことができます。ループ本体を2回繰り返すだけです。 興味深い事実:CLR2 JIT-x86には、人生を台無しにし、代わりに0 1 1 0



2 0 2 0



を与えるバグがありました。 つまずくのはそれほど難しくありません。 幸いなことに、CLR 4では修正されましたが、JITの他のバージョンではまったく修正されませんでした。 .NET Framework 3.5で作業している場合(はい、まだ必要な場合もあります)、CLR2を意味することに注意してください。 このような単純なコードが次のようになることを準備する必要があります



 ; 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
      
      





一般的に、小さなサイクルを解くというトピックは特に興味深いものです。 JIT-x86はそれらをほどくのが好きですが(大きなサイクルをほどくのは困難ですが、小さなサイクルではより簡単です)、RyuJIT(32ビットJITのコードベースに基づいています)はそれらをほどくことを拒否します。 しかし、ここのJIT-x64は私たちを喜ばせます。 彼はコードを取ることができると言います



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





値を計算します。



 ; 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
      
      





しかし、RyuJITがJIT-x64よりも悪いとは思わないでください。 はい、新世代のJITコンパイラの最適化では、すべてがそれほど良くありませんが、平均して、病院のコードはより健全です。 小さなループの巻き戻しの詳細については、こちらをご覧ください。

.NET内部について詳しく知りたいですか?



その後、私たちの光に来てください! 間もなく一連のCLRium#2セミナーがモスクワ(4月3〜4日 )、エカテリンブルグ(5月17日)、およびサンクトペテルブルク(5月29〜30日)で開催されます(オンライン放送が含まれます)。 .NETの将来について説明します。新しいCoreCLRの構造、RyuJIT機能、ハードコアRoslynの例、CoreFxの子孫について説明します。 興味深い有用な知識の無限のストリームは、独自のC#プログラムがどのように機能するかをよりよく理解するだけでなく、プラットフォームのフルパワーを使用できる明るい.NETの未来に備えることにも役立ちます!






All Articles