- 短いメソッドがインラインにならない理由とその回避方法
- 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-x86とstargのインライン化に関するストーリー
- .NETリファレンスソース:整数値からDecimalを構築します
- CoreCLR、JITソース:flowgraph.cpp(2015年2月26日)
- CoreCLR、JITソース:importer.cpp(2015年2月26日)
- MSDN:starg
- MSDN:ldarga
- Stackoverflow:.NETローカル変数の最適化
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; } }
追加資料:
- JIT-x64のバグに関するストーリー
- 実行時のJITバージョンの決定
- Stackoverflow:JIT .Netコンパイラのバグ?
- MS Connect:x64ジッター部分式除去オプティマイザーのバグ
- StackOverflow:どの.NETランタイムが使用されているかを検出する方法(MSとMono)
- StackOverflow:ryujitがアプリを起動していることを確認するにはどうすればよいですか?
巻き戻しサイクル
ループの巻き戻しは非常に優れた最適化であり、多くのコンパイラーが好んでいます。 一番下の行は、フォームのループを置き換えることです
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にいくつかの最適化がないため、プログラムの速度がわずかに低下する可能性があることを理解する必要があります。 便利なリンク:
- RyuJIT CTP5および巻き戻しループ
- ウィキペディア:巻き戻しサイクル
- ウィキペディア:ループの展開
- JC Huang、T。Leng、一般化ループ展開:プログラム高速化の方法(1998)
- ウィキペディア:分岐予測
- ウィキペディア:命令レベルの並列処理
- ウィキペディア:インライン展開
- ウィキペディア:キャッシュミス
- StackOverflow:http://stackoverflow.com/questions/2349211/when-if-ever-is-loop-unrolling-still-useful
- Blogs.Msdn:RyuJIT:.NET用の次世代JITコンパイラー
より興味深い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の未来に備えることにも役立ちます!