IL (現在は MSILではなく CIL、Common Intermediate Language として知られている) は、架空のスタック マシンでの操作を記述します。JIT コンパイラは IL 命令を受け取り、それをマシン コードにコンパイルします。
メソッドを呼び出すとき、JIT コンパイラは呼び出し規約に従う必要があります。この規則は、引数が呼び出されたメソッドに渡される方法、戻り値が呼び出し元に返される方法、およびスタックから引数を削除する責任があるユーザー (呼び出し元または呼び出し先) を指定します。この例では、cdecl呼び出し規則を使用していますが、実際の JIT コンパイラは他の規則を使用しています。
一般的方法
正確な詳細は実装によって異なりますが、CIL をマシン コードにコンパイルするために .NET および Mono JIT コンパイラで使用される一般的なアプローチは次のとおりです。
- スタックを「シミュレート」し、それを使用してすべてのスタックベースの操作を仮想レジスタ(変数) の操作に変換します。理論上、仮想レジスタは無数にあります。
- すべての IL 命令を同等の機械語命令に変換します。
- 各仮想レジスタを実マシン レジスタに割り当てます。使用可能なマシン レジスタの数は限られています。たとえば、32 ビット x86 アーキテクチャには 8 つのマシン レジスタしかありません。
もちろん、これらのステップの間には多くの最適化が行われています。
例
これらの手順を説明するために、例を挙げてみましょう。
ldarg.1 // Load argument 1 on the stack
ldarg.3 // Load argument 3 on the stack
add // Pop value2 and value1, and push (value1 + value2)
call int32 MyMethod(int32) // Pop value and call MyMethod, push result
ret // Pop value and return
ステップ 1 では、IL はレジスタベースの操作に変換されます ( operation dest <- src1, src2
)。
ldarg.1 %reg0 <- // Load argument 1 in %reg0
ldarg.3 %reg1 <- // Load argument 3 in %reg1
add %reg0 <- %reg0, %reg1 // %reg0 = (%reg0 + %reg1)
// Call MyMethod(%reg0), store result in %reg0
call int32 MyMethod(int32) %reg0 <- %reg0
ret <- %reg0 // Return %reg0
次に、x86 などの機械語命令に変換されます。
mov %reg0, [addr_of_arg1] // Move argument 1 in %reg0
mov %reg1, [addr_of_arg3] // Move argument 3 in %reg1
add %reg0, %reg1 // Add %reg1 to %reg0
push %reg0 // Push %reg0 on the real stack
call [addr_of_MyMethod] // Call the method
add esp, 4
mov %reg0, eax // Move the return value into %reg0
mov eax, %reg0 // Move %reg0 into the return value register EAX
ret // Return
次に、各仮想レジスタ %reg0、%reg1 にマシン レジスタが割り当てられます。例えば:
mov eax, [addr_of_arg1] // Move argument 1 in EAX
mov ecx, [addr_of_arg3] // Move argument 3 in ECX
add eax, ecx // Add ECX to EAX
push eax // Push EAX on the real stack
call [addr_of_MyMethod] // Call the method
add esp, 4
mov ecx, eax // Move the return value into ECX
mov eax, ecx // Move ECX into the return value register EAX
ret // Return
こぼれる
レジスタを慎重に選択することで、一部のmov
命令を削除できます。コード内の任意の時点で、使用可能なマシン レジスタよりも多くの仮想レジスタが使用されている場合、1 つのマシン レジスタをスピルして使用する必要があります。マシン レジスタがスピルされると、レジスタの値を実際のスタックにプッシュする命令が挿入されます。後で、こぼれた値を再度使用する必要がある場合、実際のスタックからレジスタの値をポップする命令が挿入されます。
結論
ご覧のとおり、機械語コードは、IL コードが評価スタックを使用する頻度ほど実際のスタックを使用していません。その理由は、マシン レジスタはプロセッサの最速のメモリ要素であるため、コンパイラは可能な限りそれらを使用しようとするためです。値は、マシン レジスタが不足している場合、または値をスタックに格納する必要がある場合 (呼び出し規則などにより) にのみ、実際のスタックに格納されます。