C#とILとネイティブと
岩永 信之
本日の内容
• C#コードのコンパイル過程
• どうしてIL (.NETの中間言語)なのか
• ライブラリのコード変更の影響
• ソースコード配付でない理由
• ネイティブ コード配付でない理由

• ネイティブ コード化
• Ngen、MDIL、Project “N”
C#、ILの
コンパイルの過程
C# → IL → ネイティブ
コンパイル
C#
コード
C#コンパイラー

IL
JITコンパイラー

ネイティブ
コード
コンパイル
.NET言語
C#の他にも
• Visual Basic
C#コンパイラー • F#
• …

C#
コード

IL

static int GetVolume(Point p)
JITコンパイラー {
return p.X * p.Y * p.Z;
}
ネイティブ

コード
コンパイル
C#
コード
C#コンパイラー

IL
JITコンパイラー

ネイティブ
コード

Intermediate Language
.NET共通の中間言語
.maxstack
IL_0000:
IL_0001:
IL_0006:
IL_0007:
IL_000c:
IL_000d:
IL_000e:
IL_0013:
IL_0014:

8
ldarg.0
ldfld int32 Point::X
ldarg.0
ldfld int32 Point::Y
mul
ldarg.0
ldfld int32 Point::Z
mul
ret
コンパイル
C#
コード
C#コンパイラー

IL
JITコンパイラー

ネイティブ
コード

ネイティブ コード
CPUごとの命令列
push
mov
cmp
je
call
mov
lea
imul
lea
imul
pop
ret

ebp
ebp,esp
dword ptr ds:[5011058h],0
00FE2A01
74B7AEA8
eax,dword ptr [ebp+8]
edx,[ebp+8]
eax,dword ptr [edx+4]
edx,[ebp+8]
eax,dword ptr [edx+8]
ebp
0Ch
例
• (C#で)こんな型があったとして
public struct Point
{
public int X;
public int Y;
public int Z;
}

• 整数のフィールドを3つ持つ
例
• こんなメソッドを書いたとする
static int GetVolume(Point p)
{
return p.X * p.Y * p.Z;
}

• フィールドの掛け算
IL
• C#コンパイル結果のIL
.method private hidebysig static int32
GetVolue(valuetype Point p) cil managed
{
.maxstack 8
型とかフィールド
IL_0000: ldarg.0
IL_0001: ldfld
int32 Point::X
の名前がそのまま
IL_0006: ldarg.0
残ってる
IL_0007: ldfld
int32 Point::Y
IL_000c: mul
型情報
IL_000d: ldarg.0
メタデータ
IL_000e: ldfld
int32 Point::Z
IL_0013: mul
IL_0014: ret
}
ネイティブ コード
• JIT結果 (x64の場合)
push
mov
cmp
je
call
mov
lea
imul
lea
imul
pop
ret

ebp
ebp,esp
dword ptr ds:[5011058h],0
00FE2A01
74B7AEA8
eax,dword ptr [ebp+8]
edx,[ebp+8]
eax,dword ptr [edx+4]
edx,[ebp+8]
eax,dword ptr [edx+8]
ebp
0Ch

4とか8とかの数値に
型情報は残らない
メモリ レイアウト
• この4とか8の意味

Point
X

public struct Point
{
public int X;
public int Y;
public int Z;
}

Y

4バイト

8バイト

Z

※レイアウトがどうなるかは環境依存
メモリ レイアウト
• この4とか8の意味
ILの時点までは名前
で参照してる
public struct Point

Point
X

{
public int X;
public int Y;
public int Z;
}

Y

4バイト

8バイト

ネイティブ コードは
レイアウトを見て
Z
数値で参照してる
※レイアウトがどうなるかは環境依存
数値でのフィールド参照
• C#で擬似的に書くと
static int GetVolume(Point p)
{
return p.X * p.Y * p.Z;
}
var pp = (byte*)&p;
var x = *((int*)pp);
var y = *((int*)(pp + 4));
var z = *((int*)(pp + 8));
return x * y * z;

4とか8とかの数値に

※これ、一応C#として有効なコード(unsafe)
ここまでまとめ
• メモリ レイアウト
C#
コード

IL

ネイティブ
コード

ILまでは型情報を残している
フィールド名で値を参照

ネイティブ コードになると型情報が残らない
レイアウト上のオフセット数値で値を参照
おまけ: ILの見方
• ildasm.exe
• Program Files以下
• Microsoft SDKsWindows[Windowsのバージョ
ン]binNETFX[.NETのバージョン]
おまけ: Nativeの見方
• Visual Studioのメニューから
• デバッグ実行時に
• [デバッグ] → [ウィンドウ] → [逆アセンブル]
• Ctrl+Alt+D
コード変更の影響
依存関係
• 開発体制としてありがちな状況
他社製
ライブラリ

自社製
ライブラリ

アプリ
依存関係
• もちろん、実際はもっと複雑な依存関係が
• 多対多
• 多段

• 開発者と利用者は別
•
•
•
•

別人
別チーム
別会社
他国
今回の例でいうと
• Point型の開発者と利用者がわかれてると仮定
他社製
ライブラリ
public struct Point
{
public int X;
public int Y;
public int Z;
}

自社製
ライブラリ
int GetVolume(Point p)
{
return p.X * p.Y * p.Z;
}

• この状況で、Point型への変更が
GetVolumeメソッド側に及ぼす影響を考える
変更してみる
• 大して影響しなさそうな
ほんの些細な変更をしてみる
public struct Point
{
public int X;
public int Y;
public int Z;
}

public struct Point
{
public int X;
public int Z;
public int Y;
}
フィールドの順序変更
その結果起きること
• メモリ レイアウトが変わる※
Point

Point

X

Y

Z

Z

※

X

Y

この例(フィールド)以外にも、仮想メソッド テーブルとかいろいろ変わる
C#レベルでの影響
• 影響なし

• Point構造体がX, Y, Zという名前の3つのintを持って
ることには変わりない
利用側(GetVolumeメソッド側)は
変更があったことを知る必要すらない
再コンパイルの必要なし
ILレベルでの影響
• 影響なし
IL_0000:
IL_0001:
IL_0006:
IL_0007:
IL_000c:
IL_000d:
IL_000e:
IL_0013:
IL_0014:

ldarg.0
ldfld
ldarg.0
ldfld
mul
ldarg.0
ldfld
mul
ret

int32 Point::X
int32 Point::Y

int32

名前で参照してるん
だから特に影響ない
Point::Z
JITが吸収してくれる
ネイティブ レベルでの影響
• ここで影響が出る
push
mov
cmp
je
call
mov
lea
imul
lea
imul
pop
ret

ebp
ebp,esp
dword ptr ds:[5011058h],0
00FE2A01
74B7AEA8
eax,dword ptr [ebp+8]
edx,[ebp+8]
eax,dword ptr [edx+4]
8
edx,[ebp+8]
更新が必要
4
eax,dword ptr [edx+8]
ebp
0Ch
利用側の再コンパイルが必要
ライブラリ側だけの差し替えじゃダメ
ここまでまとめ
• プログラムのほんの些細な変更によって
• メモリ レイアウトが変化する
• 利用側(ライブラリ側とは別人が保守)に影響
C#、JITレベルだと影響なし
ネイティブ レベルだと影響が出る
• .NET製プログラム/ライブラリ
をILの状態で配布する
最大の理由
• 事前のネイティブ化が難しい
ソースコード配布
スクリプト言語ならそんな問題こと考えなくていいよ?
ただし… 遅い
ネイティブ配布しないとして
• ILなんていう中途半端な状態にする必要ある
の?
• スクリプト言語みたいにソースコード配付すれば?

• 理由はパフォーマンス
• 測ってみよう
方法
• C#には動的に(C#プログラム中で)コンパイ
ルする仕組みがいくつかあるので
• C#コードをコンパイル
• 構文木からコード生成
• ILコンパイル
この辺りを使って比較
コンパイルの過程
高級言語
(C#)

parse
構文木

emit
IL

JIT
ネイティブ
コード
コンパイルの過程
高級言語
(C#)

parse
構文木

emit
IL

JIT
ネイティブ
コード

• parse (構文解析、文法説明)
• (C#みたいな)自然言語に近い
文法を解析
• コンパイラーが扱いやすい
データ形式に変換
コンパイルの過程
高級言語
(C#)

parse
構文木

emit
IL

JIT
ネイティブ
コード

• emit (放射、発行)
• コンピューターが解釈できる
命令を出力する
• (.NETの場合、CPU命令じゃな
くて、ILの仮想マシン命令)
• (もちろん言語によっては直接
CPUネイティブ命令を出力)
コンパイルの過程
高級言語
(C#)

parse
構文木

emit
IL

JIT
ネイティブ
コード

• JIT (Just In Time)
• プログラムを初めて実行する
その瞬間に
• IL仮想マシン命令からCPUネ
イティブ命令に変換
動的コンパイル用ライブラリ
高級言語
(C#)

Roslyn

parse
構文木

式ツリー
(System.Linq.Expressions)

emit
ILGenerator

IL

JIT
ネイティブ
コード
例
• さっきの↓これを生成してみる
static int GetVolume(Point p)
{
return p.X * p.Y * p.Z;
}
C#コードから
• Roslyn※
var engine = new ScriptEngine();
var session = engine.CreateSession();
session.AddReference(typeof(Point).Assembly);
session.ImportNamespace("System");
session.ImportNamespace("MyNamespace");
return session.Execute<Func<Point, int>>(
"p => p.X * p.Y * p.Z");

※ コードネーム(まだ正式名称じゃない)

今開発中の新しいC#コンパイラー
C#をスクリプト言語的に実行する機能も持つ
C#コードから
• Roslyn
ポイントの行:
session.Execute<Func<Point, int>>("p => p.X * p.Y * p.Z")

C#ソースコードをコンパイル
parse→emit→JIT
C#→[parse]→構文木→[emit]→IL→[JIT]→Native
構文木から
• System.Linq.Expressions
var
var
var
var
var

t
x
y
z
p

=
=
=
=
=

typeof(Point);
t.GetField("X");
t.GetField("Y");
t.GetField("Z");
Expression.Parameter(typeof(Point));

var ex = Expression.Lambda<Func<Point, int>>(
Expression.Multiply(
Expression.Multiply(
Expression.Field(p, x),
Expression.Field(p, y)),
Expression.Field(p, z)),
p);

return ex.Compile();
構文木から
• System.Linq.Expressions
ポイントの行:
Expression.Lambda<Func<Point, int>>(
Expression.Multiply(
Expression.Multiply(
Expression.Field(p, x),
Expression.Field(p, y)),
Expression.Field(p, z)),
構文木から
• 図で表すと

匿名関数を作る
本体

パラメーター

Multiply
pから
Zを読む

p

Multiply

pから
Yを読む

pから
Xを読む

C#→[parse]→構文木→[emit]→IL→[JIT]→Native
ILを直接生成
• ILGenerator
var t = typeof(Point);
var x = t.GetField("X");
var y = t.GetField("Y");
var z = t.GetField("Z");
var m = new DynamicMethod("GetVolume", typeof(int), new[] { t });
m.DefineParameter(1, ParameterAttributes.In, "p");
var gen = m.GetILGenerator();
gen.Emit(OpCodes.Ldarg_0);
gen.Emit(OpCodes.Ldfld, x);
gen.Emit(OpCodes.Ldarg_0);
gen.Emit(OpCodes.Ldfld, y);
gen.Emit(OpCodes.Mul);
gen.Emit(OpCodes.Ldarg_0);
gen.Emit(OpCodes.Ldfld, z);
gen.Emit(OpCodes.Mul);
gen.Emit(OpCodes.Ret);
ILを直接生成
• ILGenerator
ポイントの行:
var gen = m.GetILGenerator();
gen.Emit(OpCodes.Ldarg_0);
gen.Emit(OpCodes.Ldfld, x);
gen.Emit(OpCodes.Ldarg_0);
gen.Emit(OpCodes.Ldfld, y);
gen.Emit(OpCodes.Mul);
gen.Emit(OpCodes.Ldarg_0);
gen.Emit(OpCodes.Ldfld, z);
gen.Emit(OpCodes.Mul);
gen.Emit(OpCodes.Ret);
ILを直接生成
• さっき見せたILコードと一緒
IL_0000:
IL_0001:
IL_0006:
IL_0007:
IL_000c:
IL_000d:
IL_000e:
IL_0013:
IL_0014:

ldarg.0
ldfld int32 Point::X
ldarg.0
ldfld int32 Point::Y
mul
ldarg.0
ldfld int32 Point::Z
mul
ret

gen.Emit(OpCodes.Ldarg_0);
gen.Emit(OpCodes.Ldfld, x);
gen.Emit(OpCodes.Ldarg_0);
gen.Emit(OpCodes.Ldfld, y);
gen.Emit(OpCodes.Mul);
gen.Emit(OpCodes.Ldarg_0);
gen.Emit(OpCodes.Ldfld, z);
gen.Emit(OpCodes.Mul);
gen.Emit(OpCodes.Ret);

C#→[parse]→構文木→[emit]→IL→[JIT]→Native
実行
• コード生成+呼び出しを1000回ループ
かかった時間[ミリ秒]
ILGenerator
(JIT)

39.89

Expressions
(emit→JIT)

67.94

倍遅い

2桁遅い

Roslyn
4314
(parse→emit→JIT)

※うちのPCでの計測の場合

※

測るたびに1割程度はぶれる
要するに
高級言語
(C#)

parse
構文木

emit
IL

JIT
ネイティブ
コード

ここがダントツで重い
残りの部分より2桁くらい遅い

スクリプト言語だと
このコストが丸ごとかかる
補足: このコストは初回のみ
• 何度も同じメソッドを呼ぶ場合
負担はだいぶ小さい
GetVolume(p1);
GetVolume(p2);
GetVolume(p3);
GetVolume(p4);

※

JITとか※の処理はここでだけ発生
ネイティブ化済みの
コードを実行

C#みたいにIL配布する場合はJITのみ
スクリプト言語みたいにソースコード配付する場合はparse→emit→JIT
補足: このコストは初回のみ
とはいえ
• JITですら遅いって言われる
• どうしてもネイティブと比べられる
(比較対象があると結構気になる)
• 特に、起動直後に遅くなる
(最初が肝心なのに)

• これでも、C#は構文的にparseが早い部類
(構文によってはもっと遅い)
ここまでまとめ
• やっぱ事前ネイティブ化の要求はある
• JITのコストですら嫌われる
• 特に、起動直後の遅さ

• ましてparseからやるとさらに2桁遅い
事前ネイティブ化
「やっぱり最初からネイティブにしたい」となったとき
に
選択肢
• 配付形式の選択肢
• ネイティブか
• 中間形式(IL)か
• ソースコード(スクリプト)か

どれがいい?
• 状況によるので選びたい
• さらに別の選択肢: 部分的にネイティブ化
おまけ: 連携も1つの選択肢
• スクリプト言語との連携

• DLR (Dynamic Language Runtime)
• スクリプト言語実装基板

• Iron Pythonなど
• スクリプト言語の.NET実装

• ネイティブとの連携

• WinMD (Windows Metadata)
• .NETの型情報を.NET以外でも使う

• WinRT (Windows Runtime)

• .NETからも参照しやすいネイティブ ライブラリ

連携もいいんだけど、C#使いたい…
C#でスクリプティング
• ASP.NET
• C#ソースコードのままサイトに配置可能
• 初回アクセス時が異様に遅かった※ものの…
ASP.NET 4/IIS 7.5からは自動実行に

• Azure Websites上のコードならオンライン編集可能
• Visual Studio Online “Monaco”†

• Roslyn (新しいC#コンパイラー)

• C#をスクリプト的に実行する機能も持つ
※

初回アクセス時にコンパイルしてた
コンパイル(parseから)の遅さは先ほどの例の通り
† コードネーム(まだ正式名称じゃない)
ブラウザ上で編集できるエディター
C#でネイティブ
• どの道、最終的にはネイティブ化(JIT)してるん
だから
• ネイティブ化自体はそんなに難しくない
• Mono ※はAOT†コンパイル機能を持ってる

• 問題は「コード変更の影響」で説明した通り
• ライブラリ側のレイアウトとかが変わった時の再コ
ンパイル

Ngen → Auto-Ngen → MDIL → Project “N ”
※ .NET

Framework互換のオープンソース実装
† Ahead of Time: ビルドの時点でネイティブ コードにコンパイル
補足: ネイティブだけど
• ここでいう「C#でネイティブ」「.NETアプリ
のネイティブ化」とは
• IL命令をCPUネイティブ命令に置き替えること
• CPUネイティブ命令であってもmanaged
• .NETランタイムを参照していて、メモリ操作などは
.NET Frameworkの管理下
Ngen
• Ngen.exe
• Native Image Generator
• ILを事前にネイティブ化するためのツール
• 自前管理が必要
• アプリのインストーラー※とかを作って明示的に呼び出し
• 参照しているライブラリが更新された時には呼びなおす
必要あり

• かなり面倒なのでアプリを
Ngenすることはめったにない
• .NET自体が標準ライブラリの
高速化のために使ってる
※

要するに、JITの負担を起動時じゃなくてインストール時に前倒しする
Auto-Ngen
• .NET Framework 4.5以降なら

• NgenがWindowsサービスとして常に動いてる
• アイドル時に動作

• 利用頻度の高いものを自動的にNgen
• デスクトップ アプリの場合はGACアセンブリのみ
• Windowsストア アプリの場合はすべてのアセンブリ

• よく使うアプリの起動はだいぶ早くなる
• インストール直後の起動は相変わらず遅い
MDIL (ネイティブのおさらい)
• おさらい: ネイティブ コードだと
push
mov
cmp
je
call
mov
lea
imul
lea
imul
pop
ret

ebp
ebp,esp
dword ptr ds:[5011058h],0
00FE2A01
参照しているライブラリ
74B7AEA8
eax,dword ptr [ebp+8] のレイアウトが変わった
edx,[ebp+8]
時に再コンパイルが必要
eax,dword ptr [edx+4]
edx,[ebp+8]
eax,dword ptr [edx+8]
ebp
0Ch
MDIL (部分的にネイティブ化)
• じゃあ、こんな形式があればいいんじゃ?
push
mov
cmp
je
call
mov
lea
imul
lea
imul
pop
ret

ほぼネイティブ

ebp
レイアウトのところ
ebp,esp
dword ptr ds:[5011058h],0 だけ抽象的に型情報
00FE2A01
を残しておく
74B7AEA8
eax,dword ptr [ebp+8]Point::X
int32
edx,[ebp+8]
eax,dword ptr [edx+4]Point::Y
int32
edx,[ebp+8]
eax,dword ptr [edx+8]Point::Z
int32
ebp
0Ch
MDIL (Compile in the Cloud)
• 実際、Windows Phoneのストアでは
そういう形式が使われている: MDIL
(Machine Dependent Intermediate Language)
C#コード
C#コンパイラー

開発環境でILにコンパイル

MDILコンパイラー

Windowsストア サーバー
上でILをMDIL化
(Compile in the Cloud)

IL

MDIL
リンカー

ネイティブ コード

Windows Phone実機上では
レイアウトの解決(リンク)だけ行う

• インストール直後の起動も安心
Project N
• コードネーム“Project N”※
• C#/.NETコードを直接(ビルド時に)ネイティブ化す
る話も出てるみたい
• 詳細はまだ何も語られてない
• Auto-NgenとかMDILの流れをくむものっぽい

※ http://www.zdnet.com/microsoft-shows-off-its-next-generation-project-n-compiler-technology-7000023156/
ここまでまとめ
• 事前ネイティブ化はそんなに楽じゃない
• ライブラリのコード変更の影響を吸収しないといけ
ない

• かといってアプリ起動のたびにJITしたくない
• Auto-Ngen → MDIL → Project N?

• アイドル時間を使ってネイティブ化したり
• 部分的にネイティブ化したり
まとめ
• C# → IL(中間言語) → ネイティブ
• ILの意義
• ライブラリのコード変更の影響を吸収
• ソースコード配付よりはだいぶ高速

• ネイティブ コード化

• Auto-Ngen → MDIL → Project N
• アイドル時間を使ってネイティブ化したり
• 部分的にネイティブ化したり

C#とILとネイティブと