アーカイブ | 11:55

.NETの中間言語

17 12月

Dynamicの話をしたついでですし、今日は.NETの中間言語(intermediate language: 以下、「IL」と表記)についてお話しましょう。

 

まあ、ほとんどの.NET開発者にとっては無縁な情報ですねぇ。別に見なくてもいいけど、中身もちょっと気になるって人向けの話になります。

それでもやるったらやる!それが私、C#たんっ!

 

まあ、概要程度に。C#プログラマーの人が面白がって読める程度の内容だと思います。

ILを見る機会

IL、昔は多少見る機会があったんですけども。もう、あんまり機会もないですねぇ。

IL逆アセンブラー

.NET Frameworkには、IL逆アセンブラー(ildasm.exe)が標準で付属しています。

もし、ソース コードが手に入らないライブラリの内部挙動を知る必要が出てきた場合、こいつを使えばある程度調べが付きます。

ただ、今だと、C#逆コンパイラーとかも普通にフリーで手に入ります。私の場合は、IL Spyっていうツールを使っています。ILを読むのは結構大変なわけですが、C#化されればだいぶ読めると思います。なので、わざわざIL逆アセンブラーを使う理由はあまりなくなりました。

動的コード生成

「Dynamicの話をしたついで」と言ったのは、昔(.NET 2.0まで)は、動的コード生成にILの知識が必須だったからです。

昨日、DynamicMethodってやつを紹介しましたが、これを使って動的にメソッドを作るには、ILを書かないといけませんでした。

今(3.0以降)では、式ツリーを使った動的コード生成ができるので、ILの知識は不要です。

.NET仮想マシンの仕組み

一応、ILというか、.NETの仮想マシンの仕組みを簡単に説明しておきます。

C#をはじめとする、.NET対応言語は、.NETの仮想マシンが直接解釈できるILマシン語にコンパイルされます。アプリやライブラリは、このILの状態で配布します。

一般的な(仮想マシンか実CPUか問わず)実行環境と同様に、ILには、マシン語と1対1に対応していて、ある程度人間でも読めるような言語(アセンブリ言語)が定義されています。

ILは、.NET Frameworkの仮想マシンによって、都度、ネイティブ コード(実CPUが直接解釈できるマシン語)にコンパイルしながら(Just-In-Time方式のコンパイル、略してJIT)実行されます。

.NETアセンブリの中身

C#などのソース コードのコンパイル結果、つまり、.NET向けの実行可能形式(exe)やライブラリ(dll)を総称して、.NETアセンブリ(assembly)と言います。

アセンブリの中には、以下のように、メタデータ(型情報や属性)と、ILコードが入っています。

メタデータ

昨日お話したような、実行時の型情報利用は、このメタデータの部分から読みだしています。

スタック型マシン

.NETの仮想マシンは、スタック型と呼ばれるタイプの構造をしています。スタック型マシンでは、演算や関数の引数や戻り値を、すべてスタック上に積んでいきます。

例えば、以下のようなC#コードを書いたとしましょう。

var x = int.Parse(Console.ReadLine());
var y = int.Parse(Console.ReadLine());
Console.WriteLine(“{0} + {1} = {2}”, x, y, x + y);

以下のようなILが生成されます。

.method private hidebysig static void Main() cil managed
{
.entrypoint
.maxstack 5
.locals init (int32 V_0,
int32 V_1)
IL_0000: nop
IL_0001: call string [mscorlib]System.Console::ReadLine()
IL_0006: call int32 [mscorlib]System.Int32::Parse(string)
IL_000b: stloc.0
IL_000c: call string [mscorlib]System.Console::ReadLine()
IL_0011: call int32 [mscorlib]System.Int32::Parse(string)
IL_0016: stloc.1
IL_0017: ldstr “{0} + {1} = {2}”
IL_001c: ldloc.0
IL_001d: box [mscorlib]System.Int32
IL_0022: ldloc.1
IL_0023: box [mscorlib]System.Int32
IL_0028: ldloc.0
IL_0029: ldloc.1
IL_002a: add
IL_002b: box [mscorlib]System.Int32
IL_0030: call void [mscorlib]System.Console::WriteLine(string,
object,
object,
object)
IL_0035: nop
IL_0036: ret
} // end of method Program::Main

 

このILを、幾分か端折って、実行していく様子を見ていきましょう。

最後は、3をボックス化した後、4引数のConsole.WriteLineを呼んで、スタック上の値をすべて消費して、結果をコンソールに表示します。

スタック型とレジスター型

.NETのILに限らないんですが、多くの仮想マシンはスタック型マシンになっています。

一方で、実CPUはだいたいレジスター型と呼ばれる構造をしています。スタックとは別に、レジスターと呼ばれる、高速に読み書き可能な記憶領域(個数が限られています)を持ちます。

コンパイラーがスタック型の命令を作るのは結構簡単です。仮想マシンの命令がスタック型になっていることが多いのはこのためです。レジスター型のCPU向けのコンパイラーであっても、一度スタック型の中間言語を生成した後で、実際のレジスター型の命令に置き換えるようなものも多いです。

命令長

.NETのILの命令は、マシン語にしたとき、ほとんどが1バイト命令です(使用頻度の低いいくつかの2バイト命令、4バイト命令もあります)。また、一般的に、スタック型マシンのコードは、レジスター型マシン(たいていの実CPUはレジスター型)よりも短くなります。

後述するように、かなり高級な命令セットを持っているのもあって、ILコードはかなりコンパクトになります。実CPUのネイティブ コードと比べると、下手すると10倍くらいは小さくなります。

(もっとも、その分、メタデータがでかいので、全体としては10倍とは行きませんが。)

アセンブリ言語だけど、かなり高級言語

一般的にいうと、マシン語(アセンブリ言語)は、実行環境でできることそのままな、低級言語です(ここでいう高低は、ハードウェアに近いか(低)、遠いか(高)を表します)。しかし、ILの場合は、そもそも.NETの仮想マシンがかなり高級です。なので、ILも、下手するとC言語のようなネイティブ言語よりもよっぽど高級です。

例えば、以下のような仕組みを持っています。

  • (前述の例を見ての通り)スタックに任意の型を格納できる
  • オブジェクト指向命令
    • フィールドの読み書きや、メソッドの呼び出しが1命令でできます
    • 仮想メソッドの呼び出しも1命令です
  • 例外処理の機構を持っています
  • ジェネリックにILレベルで対応しています
    • 共変性・反変性のフラグも持っています

 

(ちなみに、ジェネリック対応以外は、Java仮想マシンもだいたい同じような仕組みを持っています。)

C#との対比

あとの細かい点は、C#と対比しながら話しましょうか。

  • intなどのいくつかの型は、加減乗除など、専用の命令を持っています(プリミティブ型と呼ばれます)
    • ちなみに、decimalはプリミティブではないです。ただの構造体
  • デリゲート、列挙型は特別扱いを受けています
    • 内部的にクラスになったりするわけではなく、別定義
    • 一方、null許容型はただの構造体です
  • 配列も結構特殊な扱いを受けています
  • 文字列(stringクラス)は単なるクラスですが、リテラルの扱いが多少特殊です
    • C#で、文字列だけは参照型なのにconstを付けれるのはそのため
  • ポインター型もあります
  • 加減乗除などの命令には、オーバーフローのチェックをするタイプとしないタイプとがあります
    • チェックするタイプでは、オーバーフロー時に例外を発生させます
  • switchステートメント相当の専用命令があります
    • 状況に応じて、if-else的なコードにJITするか、ジャンプ テーブル的なコードにJITするか選んでくれるそうです

 

C#にはあるけど、ILには直接はないもの:

  • インデクサー
    • 内部的にはプロパティ扱いです
  • ユーザー定義の演算子
    • 単なる静的メソッドになります
  • ループ(while, for, foreach)
    • ifとgoto的なコードに展開されます
  • ラムダ式、イテレーター、非同期メソッド、dynamic
    • かなり込み入ったコード生成してます
    • イテレーターとかラムダ式に至っては、クラスが生成されたりしてます
  • クエリ式
    • 割かし単純なメソッド呼び出しへの置き換え
  • lockステートメント
    • Monitorクラスに丸投げ
  • 拡張メソッド、デフォルト引数、dynamicな引数
    • 属性が付くだけ

 

ILにはあるけど、C#にはないもの:

  • 本当の意味での可変長引数
    • C#のものは、配列の自動生成で実装しています
    • ILレベルでは、任意個数のスタックを消費してメソッドを呼び出す方法があります
  • 変数の間接参照
    • C#だと、引数を参照渡しするときしか間接参照がおきませんが、ILレベルではもっといろいろ使える参照ロード/ストア命令があります
  • デリゲートとは別に、関数ポインターを持っています