C#コンパイラ自体は、リリースビルドで発行されたILを大幅に変更しません。注目すべきは、中括弧にブレークポイントを設定できるNOPオペコードを発行しないことです。大きなものは、JITコンパイラに組み込まれているオプティマイザです。私はそれが次の最適化を行うことを知っています:
メソッドのインライン化。メソッド呼び出しは、メソッドのコードを挿入することで置き換えられます。これは大きな問題であり、プロパティアクセサを本質的に無料にします。
CPUレジスタの割り当て。ローカル変数とメソッド引数は、スタックフレームに戻されることなく(または頻繁に)戻されることなく、CPUレジスターに格納されたままにすることができます。これは大きな問題であり、最適化されたコードのデバッグを非常に困難にすることで注目に値します。そして、volatileキーワードに意味を与えます。
配列インデックスチェックの削除。配列を操作する際の重要な最適化(すべての.NETコレクションクラスは内部で配列を使用します)。JITコンパイラーは、ループが範囲外の配列にインデックスを付けないことを確認できる場合、インデックスチェックを排除します。大きい。
ループ展開。小さな本体のループは、本体でコードを最大4回繰り返し、ループ回数を減らすことで改善されています。分岐コストを削減し、プロセッサのスーパースカラー実行オプションを改善します。
デッドコードの除去。if(false){/ ... /}のようなステートメントは完全に削除されます。これは、一定の折りたたみとインライン化が原因で発生する可能性があります。その他の場合は、JITコンパイラがコードに副作用がないと判断できる場合です。この最適化のため、プロファイリングコードは非常に扱いにくくなっています。
コードの巻き上げ。ループの影響を受けないループ内のコードは、ループの外に移動できます。Cコンパイラのオプティマイザは、巻き上げる機会を見つけるためにより多くの時間を費やします。ただし、データフロー分析が必要なため、費用のかかる最適化であり、ジッターでは時間を確保できないため、明らかなケースのみを巻き上げます。.NETプログラマーに、より優れたソースコードを記述し、自分自身を巻き上げるように強制する。
一般的な部分式の除去。x = y + 4; z = y + 4; z = xになります。dest [ix + 1] = src [ix + 1];のようなステートメントではかなり一般的です。ヘルパー変数を導入せずに読みやすくするために記述されています。読みやすさを損なう必要はありません。
一定の折りたたみ。x = 1 + 2; x = 3になります。この簡単な例は、コンパイラによって早期に検出されますが、他の最適化がこれを可能にするJIT時に発生します。
伝播をコピーします。x = a; y = x; y = aになります。これは、レジスタアロケータがより適切な決定を行うのに役立ちます。処理するレジスタが少ないため、x86ジッタでは大きな問題です。適切なものを選択することが、パフォーマンスにとって重要です。
これらは非常に重要な最適化であり、たとえば、アプリのデバッグビルドのプロファイルを作成し、それをリリースビルドと比較する場合に大きな違いを生む可能性があります。それが本当に重要なのは、コードがクリティカルパス上にある場合、作成したコードの5〜10%が実際にプログラムのパフォーマンスに影響を与える場合です。JITオプティマイザーは、何が重要であるかを事前に把握するほどスマートではありません。すべてのコードに「11に回す」というダイヤルしか適用できません。
プログラムの実行時間に対するこれらの最適化の効果的な結果は、他の場所で実行されるコードの影響を受けることがよくあります。ファイルの読み取り、dbaseクエリの実行など。JITオプティマイザーが行う作業を完全に非表示にします。それは気にしません:)
JITオプティマイザーはかなり信頼できるコードです。これは、何百万回もテストされているためです。プログラムのリリースビルドバージョンで問題が発生することは非常にまれです。しかし、それは起こります。x64とx86の両方のジッターは、構造体に問題がありました。x86ジッタは浮動小数点の一貫性に問題があり、浮動小数点計算の中間体がメモリにフラッシュされるときに切り捨てられるのではなく、80ビット精度でFPUレジスタに保持される場合、微妙に異なる結果を生成します。