10

私はILがどのように機能するかを調べるための簡単なプログラムを書きました:

void Main()
{

 int a=5;
 int b=6;
 if (a<b) Console.Write("333");
 Console.ReadLine();
}

IL:

IL_0000:  ldc.i4.5    
IL_0001:  stloc.0     
IL_0002:  ldc.i4.6    
IL_0003:  stloc.1     
IL_0004:  ldloc.0     
IL_0005:  ldloc.1     
IL_0006:  bge.s       IL_0012
IL_0008:  ldstr       "333"
IL_000D:  call        System.Console.Write
IL_0012:  call        System.Console.ReadLine

私は実装された効率を理解しようとしています:

  • 1行目(ILコード)で、値5をスタックにプッシュします(int32である4バイト)

  • 2行目(ILコード)で、スタックからローカル変数にPOPします。

次の2行も同じです。

次に、それらのローカル変数をスタックにロードしてから、評価bge.sます。

質問1

なぜ彼はローカル変数をスタックにロードするのですか?値はすでにスタックにあります。しかし、彼はそれらをローカル変数に入れるためにそれらをポップしました。無駄じゃないですか?

つまり、コードが次のようなものになり得なかった理由:

IL_0000:  ldc.i4.5
IL_0001:  ldc.i4.6    
IL_0002:  bge.s       IL_0004
IL_0003:  ldstr       "333"
IL_0004:  call        System.Console.Write
IL_0005:  call        System.Console.ReadLine

私のコードのサンプルはたった5行のコードです。50,000,000行のコードはどうですか?ILによって発行される余分なコードがたくさんあります

質問2

コードアドレスを見てください:

ここに画像の説明を入力してください

  • IL_0009アドレスはどこにありますか?シーケンシャルになっているのではないですか?

+リリースモードで最適化フラグを使用したpsIm

4

4 に答える 4

10

2番目の質問には簡単に答えることができます。命令は可変長です。たとえば、は(アドレスでldstr "333"の)オペコードと、それに続く文字列を表すデータ(ユーザー文字列テーブル内の文字列への参照)で構成されます。ldstr8

call次のステートメントと同様に、callオペコード自体に加えて、呼び出す関数に関する情報が必要です。

4や6などの小さな値をスタックにプッシュするための命令に余分なデータがない理由は、値がオペコード自体にエンコードされているためです。

手順とエンコーディングについては、こちらをご覧ください。

最初の質問については、C#開発者の1人であるEricLippertによるこのブログエントリをご覧ください。

/ optimizeフラグは、放出および生成ロジックの大部分を変更しません。私たちは常に単純で検証可能なコードを生成しようとし、実際のマシンコードを生成するときに、ジッターに依存して最適化を大幅に強化します。

于 2012-12-08T11:46:07.260 に答える
7

なぜ彼はローカル変数をスタックにロードするのですか?値はすでにスタックにあります。しかし、彼はそれらをローカル変数に入れるためにそれらをポップしました。もったいないではないですか?

何の無駄?ILは(通常)そのままでは実行されず、ほとんどの最適化を実行するJITコンパイラによって再度コンパイルされることを覚えておく必要があります。「中間言語」を使用するポイントの1つは、最適化を1か所で実装できるようにすることです。JITコンパイラと各言語(C#、VB.NET、F#など)で、最適化を再度実装する必要はありません。これは、EricLippertの記事WhyILで説明されています。

IL_0009アドレスはどこにありますか?シーケンシャルではないですか?

ldstr命令の仕様を見てみましょう( ECMA-335から):

III.4.16 ldstr–リテラル文字列をロードします

フォーマット:72<T>[…]

このldstr命令は、メタデータに格納されているリテラルを文字列(文字列リテラル)として表す新しい文字列オブジェクトをプッシュします。

上記のメタデータと<T>への参照は、命令のバイト72の後に、文字列を含むテーブルを指すメタデータトークンが続くことを意味します。そのようなトークンはどれくらいの大きさですか?同じ文書のセクションIII.1.9から:

多くのCIL命令の後には、「メタデータトークン」が続きます。これは4バイトの値であり、メタデータテーブルの行を指定します[…]

したがって、あなたの場合、72命令のバイトはアドレス0008にあり、トークン(この場合、0x70000001、0x70バイトはユーザー文字列テーブルを表します)はアドレス0009〜000Cにあります。

于 2012-12-08T13:04:03.713 に答える
6

このレベルでのIL効率について推論する意味はありません。

JITはスタックを完全に排除し、すべてのスタック操作を中間の3アドレスコードに変換します(さらにはSSAに変換します)。ILが解釈されることはないため、スタック操作は効率的で最適化されているとは考えられていません。

たとえば、オープンソースのMono実装を参照してください。

于 2012-12-08T11:58:36.347 に答える
0

「追加コード」に関するこのすべての議論に最終的な答えを与えること。

C#コンパイラはそれを読み取り、次のようint a=5;に変換します。

ldc.i4.5
stloc.0

次に、次の行に移動して読み取り、次のようにint b=6;変換されます。

ldc.i4.6
stloc.1

次に、ifステートメントなどを使用して次の行を読み取ります。

C#からILにコンパイルするときは、行ごとに読み取り、他の行を見るときはその行ではなく、その行をILに変換します。

この段階でILを最適化し、「余分なコード」(これを呼び出す)を削除するには、C#コンパイラはすべてのILコードをチェックし、そのツリー表現を構築し、不要なノードをすべて削除してから、ILとして再度書き込む必要があります。 。これは、ILから機械語に移行するときにJITコンパイラによって実行されるため、C#コンパイラが実行する必要があることではありません。

したがって、余分と​​見なされるコードは余分なコードではなく、C#コンパイラがC#コードから読み取ったステートメントの一部であり、JITコンパイラがコードをネイティブ実行可能ファイルにコンパイルするときに削除されます。

これは、コンパイラの構築などでクラスを受講したことがないと思うので、C#コードがどのように変換されるかについての高レベルの説明でした。もっと知りたい場合は、インターネット上に読むべき本やページがあります。

于 2012-12-09T16:13:23.573 に答える