Skip to main content
C#や.NET Framework
がやっていること
第二版
岩永 信之
今日の内容
• C#や.NET Frameworkがやっていること
• どうしてそういう機能がある
• (自前で実装するなら)どうやればできる
• (自前でやる上で) 気を付ける必要がある点
注意
• ベースは3月にBoost勉強会でやった内容
• 通常の3倍速で1時間で話した
• 本来、1時間で話す内容じゃない
• かなりの前提知識を仮定
• 3時間話します
• いくつか追記
• 前提をだいぶ緩めたので背景説明など
• .NET vNextがらみ追加
• 5月の非同期勉強会での内容も少しマージ
同じような内容でも回数重ねてこなれてきてたり
目次
• 1時間目
• メモリ管理
• マルチ タスク管理
• 2時間目
• メタデータ
• 3時間目
• C#言語機能
1
2
3
1時間目
• メモリ管理
• ヒープ自動管理
• 境界チェック
• ドメイン分離
• マルチ タスク管理
• スレッドと非同期処理
1
2
3
“Managed”
• .NET FrameworkといえばManaged
• .NETがいろいろ管理
• メモリ リーク防止
• セキュリティ ホール防止
• v1リリース後、一番進歩が激しいのは非同期
• 効率的なタスク管理
• I/O-boundな非同期処理
Garbage Collection
メモリ リークをなくすために
メモリ管理(1)
• スタック
• 一番上に積む・一番上から取り出す
• ローカル変数用
• 管理は楽
• スコープがはっきりしてないと使えない
int x = 1;
int y = 2;
int z = 4;
int sum = x + y + z; 1 1
2
load 1 load 2
3
add
3
4
load 4
7
add
メモリ管理(2)
• ヒープ
• 連結リスト構造で任意の位置にメモリ確保
• 動的なメモリ利用できる
• 管理が大変
• 消し忘れ(メモリ リーク)があり得る
使用中かどうかのフラグ
☑□ □ ☑
次のヒープの位置
前のヒープの位置
… …
値型と参照型
• 値型
• intとかbyteとか
• structとかenumとか
スタック ヒープ
他の型
他の型
スタック上や、他の型の
中に直接埋め込まれる
• 参照型
• stringとか
• classとかdelegateとか
スタック ヒープ
スタック上や他の型の中
には参照情報だけある
ヒープ上に置かれる
ヒープ管理
• 怠ると
• 徐々にアプリが重くなる
• 長時間稼働させると突然の死
• その割に大変
• (自動管理がない頃)
プログラミングの苦労の半分くらい占める
• 本当にやりたいことに注力できてない
• 自動化したい
不要なオブジェクトの回収 =ゴミ を集め(garbage collection)
代表的な手法
• 参照カウント
• 生成・コピー代入のたびにカウントを +1
• 変数がスコープ外れるたびに -1
• カウントが0になったらメモリ削除
• Mark & Sweep
• ルート†からたどれるオブジェクトに印を付ける
• 印のついてないオブジェクトを削除
† スタックや静的フィールド中のオブジェクトなど
参照の起点となる場所
mark
sweep
Mark & Sweep
• ルートから参照をたどる
Mark
ヒープ
ルート
Sweep
使用中オブジェクト
未使用オブジェクト
Compaction
• GC時にオブジェクトの位置を移動
使用中オブジェクト
未使用オブジェクト
隙間をなくすように移動
後ろが丸ごと空くので次のメモリ確保が楽
Generation
• 移動したオブジェクトはしばらくノータッチ
Gen1 Gen0
Gen0
しばらくここは
ノータッチ
この範囲でだけ
メモリ確保・GC
オブジェクトの寿命
統計的に
• 短いものはとことん短く
• 長いものはとことん長い
一度GCかかったオブジェクト
はしばらく放置
.NET FrameworkのGC
• .NET FrameworkのGCはMark & Sweep
• Compactionあり
• 世代別 (3世代、Gen0~2)
• Backgroundスレッドで並列実行
Mark & Sweepと参照カウント
• 比較
• 一長一短ある
Mark & Sweep 参照カウント
メモリ確保 ○ 末尾を見るだけ × 空いている場所を探す
× カウント用の領域が追加
で必要
変数のコピー ○ ポインターのコピー × ポインターのコピーに加
え、参照数のカウント
アップ
メモリ解放 × MarkやCompactionに
時間がかかる
× 負担が1か所に集中
○ カウントが0になった時
にdeleteするだけ
× 循環参照が苦手
Throughput
• トータルの性能(throughput)はMark & Sweep
の方がいい
Mark & Sweep 参照カウント
メモリ確保 ○ 末尾を見るだけ × 空いている場所を探す
× カウント用の領域が追加
で必要
変数のコピー ○ ポインターのコピー × ポインターのコピーに加
え、参照数のカウント
アップ
メモリ解放 × MarkやCompactionに
時間がかかる
× 負担が1か所に集中
○ カウントが0になった時
にdeleteするだけ
× 循環参照が苦手
まとめてやる方がバラバラに
やるよりスループットはいい
頻度が高い操作なので
ここの負担が大きいと
全体の性能落ちる
特に、スレッド安全を求めるときつい
たかがインクリメントでも、atomic性
保証するとそこそこの負担
参照局所性も高くなって
キャッシュが効きやすい
短所の軽減
• 短所も、まったく打つ手がないわけじゃない
Mark & Sweep 参照カウント
メモリ確保 ○ 末尾を見るだけ × 空いている場所を探す
× カウント用の領域が追加
で必要
変数のコピー ○ ポインターのコピー × ポインターのコピーに加
え、参照数のカウント
アップ
メモリ解放 × MarkやCompactionに
時間がかかる
× 負担が1か所に集中
○ カウントが0になった時
にdeleteするだけ
× 循環参照が苦手
.NET 4以降、Background
スレッド実行してる
C++ 11的には
「move semantics活用
してね」
自前メモリ管理との混在
• スコープが短いならスタックにとればいいのに
• C#はclassは必ずヒープ、structはスタックになる
• 使い分けれない
• Mark & Sweep管理領域内のポインターを管理
外に渡すときには注意が必要
• Compactionでメモリ移動しないように
「ピン止め」が必要
• 結構ガベージ コレクションのパフォーマンス落とす
おまけ: 他の言語
• C++: 主に参照カウント方式のライブラリ利用
• 型がはっきりしない言語ではMark & Sweepしにくい
• できなくはないけども「保守的」になる(効率落とす)
• 数値を無差別にポインター扱いしてMarkする
• Pythonとか: 参照カウントとMark & Sweepの併用
• ○ 循環参照問題避けつつ、負担を1か所に集中させない
• × 性能的には悪いとこどり
• Go: 割当先を自動判別
• スコープ内で完結してたらスタックに、さもなくばヒープに
• これはこれでスタックの浪費激しそうなんだけども…
• なので、可変長スタック持ってる
注意: GCあってもリーク
• 長時間生きているオブジェクト†が参照持った
ままになると、GC対象になれない
† ワーストケースはアプリ稼働中ずっと生きているオブジェクト
class View : UserControl
{
public View(Model model)
{
model.PropertyChanged += (sender, e) =>
{
// データの表示を更新
};
}
}
よく起こり得る例
ビューは画面遷移で消える
モデルはずっと生きてる
イベント購読で、
モデルがビューの参照持つ
解除 (-=) しないとメモリ リーク
ポイント
• メモリ管理は大変なので自動化
• Mark & Sweep
• いろいろ賢いアルゴリズム搭載してるし、ほとんど
の場合、任せた方がいい
• GCあってもメモリ リークは起こり得るので注
意
境界チェック
意図しないメモリ領域にはアクセスさせない
配列の境界チェック
• .NETの配列は厳しい
• 常に境界チェックしてる
• 要はbuffer overrun†防止
• ちなみにJIT最適化で不要な境界チェック消してる
(明らかに境界を侵さないものはチェック不要)
• for (var i; i < a.Length; ++i) とか
• foreach (var item in a) とか
配列 a
OutOfRange OutOfRange
a[-1] a[length+1]
† クラッシュやセキュリティ ホールの温床
unsafe
• とはいえ、C#にもポインターあるんだけども
var x = new[] { 1, 2, 3, 4, 5 };
unsafe
{
fixed(int* px = &x[0])
{
Console.WriteLine(px[100]);
}
}
メモリ移動の防止
(ピン止め)
範囲チェックなし
安全でないコード
この中でだけポイ
ンター利用可能
buffer overrunやりたい放題
unsafeでも制限付き
• class (参照型)はアドレス取れない
• 仮想メソッド テーブル(vtable)とか入ってるし
• 親クラス側のレイアウト変更の影響受けるし
class Class
{
public int x;
public int y;
}
var c = new Class();
fixed (void* p = &c) { } // error
fixed (void* p = &c.x) { } // OK
fixed (void* p = &c.y) { } // OK
ピン止め必須
• 値型のメンバーのアドレスは取れる
• オブジェクト自体のアドレスは取れ
ない
unsafeでも制限付き
• struct (値型)はアドレス取れる
• ただし、メンバーが全部値型の時のみ†
• ↑「unmanaged型」と呼ぶ
struct UnmanagedStruct
{
public int x;
public int y;
}
var u = new UnmanagedStruct();
void* p1 = &u; // OK
void* p2 = &u.x; // OK
void* p3 = &u.y; // OK
メンバーが
全部値型
無制限にアドレス取れる
† 正確には、再帰的に全子要素が値型、かつ、型パラメーターも値型
unsafeでも制限付き
• struct (値型)であっても制限付き
• メンバーに1つでも参照型を含むとダメ
• ↑「managed型」と呼ぶ
struct ManagedStruct
{
public int x;
public string y;
}
var u = new ManagedStruct();
void* p1 = &u; // error
void* p2 = &u.x; // OK
void* p3 = &u.y; // error
メンバーに
参照型が1つ
本体のアドレス取れない
値型のメンバーのところ
だけはアドレス取れる
ポイント
• unsafe
• 危険なことは基本認めない
• 面倒な追加の構文を要求
• コンパイル オプションでも/unsafeの明記必須
• fixed
• ガベージ コレクションとの兼ね合い
• Compaction阻害になるので注意
• 「Unmanaged型」に限る
• 実装依存な部分(vtableとか)や、
型の定義側の変更が利用側に極力影響しないように
AppDomain
実行環境の分離
セキュリティ保証
コンポーネントの連携
• 他のアプリの機能を自分のアプリから呼びたい
• WordとかExcelとかのドキュメントを出力
• サーバー上に自分のアプリを配置したい
• IIS上に(ASP.NET)
• SQL Server上に
• (必ずしも)信用できない
• セキュリティ保証
• 参照先のクラッシュにアプリ/サーバー
が巻き込まれないように
• コンポーネントのバージョン アップ
AppDomain
• 単一プロセス内で、分離された複数の実行領域
(domain)を提供
• 「分離」
• plugin: 動的なロード/アンロード
• security: AppDomainごとに異なる権限付与
• isolation: 別メモリ空間
AppDomain 1
AppDomain 2…
プロセス
必ずしも信用できないコードを
安全に、動的に呼び出し
ドメイン間には壁がある
• 別メモリ空間に分離されてる
AppDomain 1 AppDomain 2
互いに独立
ドメイン間の通信
• マーシャリング(marshaling)
• marshal (司令官、案内係)の命令通りにしか壁を超
えれない
AppDomain 1 AppDomain 2
• 司令がダメといったら通れない
• 司令の指示通りの形式にいったん
シリアライズしないといけない
AppDomain 1 AppDomain 2
ドメイン間の通信
• ダメな例
これをBに渡し
たいとして
あるオブジェクト
AppDomain 1 AppDomain 2
ドメイン間の通信
• ダメな例
shallow copyなんてしようもんなら
A側のメモリへの参照が残る
AppDomain 1 AppDomain 2
ドメイン間の通信
• よい例
serialize deserialize
{
{1, 2},
{3, 4},
{5, 6}
}
この辺りがマーシャリング
一度シリアライズ 必ずdeep copy
.NETのマーシャリング
• 2種類
• marshal by ref
• 参照を渡すんだけど、メモリ領域は直接触れない
• メソッド越しにしか操作しちゃいけない
• 「このメソッドを呼んでくれ」っていうメッセージだけ
がドメインを超えてわたって、実際の実行は相手側ドメ
インで(プロキシ実行)
• marshal by value
• 値をシリアライズして相手側に渡す
• 規定では、BinarySeralizerを使用
privateフィールドまで含めてリフレクションで
内部状態を取得してシリアライズ
.NETのマーシャリング(文字列)
• 文字列は特殊扱い
(文字コード変換が不要な場合)
• marshal by valueみたいにシリアライズを経ない
• marshal by referenceみたいにプロキシ実行しない
• 参照を渡して、メモリを直接読んでもらう
• immutableかつrange-check付きに作ってある
• 変更不可なので参照を渡しても安全
• COM (ネイティブ)に対して文字列を渡す時すらこの方式
AppDomain 1 AppDomain 2
その他のコスト
• マーシャリング以外にもいくらかコストが
• 例えばライブラリの読み込み
Library X Library X
別ドメインに全く同じライブラリを読み込んでも、
それぞれ別イメージが作られる
Global Assembly Cache (GAC)にあるDLLだけは別
GAC中のは同じメモリ イメージが共有される
おまけ: 他の言語
• COMなんかはマーシャリングやってる
• というか「COMの頃から」やってる
• .NETの前身なので
• さもなくば、プロセス分離してプロセス間通信
• OS特権がないと他プロセスのメモリにはアクセス
できない
• 安全だけどかなり高負荷
• RPC†とか一時期流行ったけども廃れた
† ただの関数呼び出しに見えるコードで、内部的にプロセス間通信する
知らないうちに性能落とすとか、リモート側でのエラーが処理しにくいとか
ポイント
• プラグインとかやるなら「分離」が必須
• とはいえ、プロセス分けると負担が大きすぎる
• プロセス内で分離を保証する仕組み
• AppDomain
• マーシャリング
• それなりにコストがかかるけども、セキュリティに
は変えられない
スレッド
応答性のよいプログラムを書くために
OSやアプリ全体をフリーズさせないために
マルチタスク
• コンピューター内で複数のタスクが同時に動作
• CPUコア数に制限されない
タスク1 タスク2 タスク3 …
タスクの動作期間
実際にCPUを使って
動いている期間
1つのCPUコアを複数の
タスクがシェアしてる
問題は
• どうやって他のタスクにCPUを譲るか
• 誰がどうスケジューリングするか
2種類のマルチタスク
† preemptive: 専買権を持つ、横取りする
※cooperative
• ハードウェア タイマーを使って強制割り込み
• OSが特権的にスレッド切り替えを行う
• ○利点: 公平 (どんなタスクも等しくOSに制御奪われる)
• ×欠点: 高負荷 (切り替えコストと使用リソース量が多い)
プリエンプティブ†
• 各タスクが責任を持って終了する
• 1つのタスクが終わるまで次のタスクは始まらない
• ○利点: 低負荷
• ×欠点: 不公平 (1タスクの裏切りが、全体をフリーズさせる)
協調的※
なのでスレッドはこっち
これが致命的
ただ、問題はこれ
問題: スレッドは高コスト†
• 細々としたタスクを大量にこなすには向かない
for (int i = 0; i < 1000; i++)
{
var t = new Thread(Worker);
t.Start();
}
大量の処理をスレッド実行
リソース消費大
切り替え頻発
…
† スレッドごとに数MBのメモリを確保したり
スレッド切り替えにOS特権が必要だったり
文脈が切り替わっちゃうのでキャッシュ ミスしたり
解決策: スレッド プール
• スレッドを可能な限り使いまわす仕組み
• プリエンプティブなスレッド数本の上に
• 協調的なタスク キューを用意
スレッド プール
キュー
タスク1
タスク2
…
数本※のスレッ
ドだけ用意
空いているスレッドを探して実行
(長時間空かない時だけ新規スレッド作成)
新規タスク
タスクは一度
キューに溜める
※ 理想的にはCPUのコア数分だけ
スレッド プールの性能向上
• Work Stealing Queue
• lock-free実装†なローカル キュー
• できる限りスレッド切り替えが起きない作り
ローカル
キュー1
ローカル
キュー2
スレッド1 スレッド2
グローバル
キュー
①
スレッドごとに
キューを持つ
まず自分用の
キューからタスク実行
②
ローカル キュー
が空のとき、
他のスレッドから
タスクを奪取
† 今回は詳細割愛
スレッド プールの性能向上
• Work Stealing Queue
• lock-free実装†なローカル キュー
• できる限りスレッド切り替えが起きない作り
ローカル
キュー1
ローカル
キュー2
スレッド1 スレッド2
グローバル
キュー
①
スレッドごとに
キューを持つ
まず自分用の
キューからタスク実行
②
ローカル キュー
が空のとき、
他のスレッドから
タスクを奪取
ポイント
• スレッドは高コスト
• Threadクラスはこっち
• スレッド プールの利用推奨
• Taskクラスはこっち
† 今回は詳細割愛
問題: CPUの外の世界は遅い
• 実行速度が全然違う
ALU
メイン・メモリ
CPU 周辺機器
数千~
下手すると数万、数億倍遅い
>>>
2種類の負荷
• CPU-bound (CPUが性能を縛る)
• マルチコアCPUの性能を最大限引き出したい
• UIスレッドを止めたくない
• I/O-bound (I/O※が性能を縛る)
• ハードウェア割り込み待つだけ
• CPUは使わない
• スレッドも必要ない
※ Input/Output: 外部ハードウェアとのやり取り(入出力)
I/O完了待ち
• I/O-boundな処理にスレッドは不要
あるスレッド
要求
応答
この間何もしないのに
スレッドを確保し続け
るのはもったいない
解決策: I/O完了ポート※
• スレッドを確保せずI/Oを待つ仕組み
• コールバックを登録して、割り込みを待つ
• コールバック処理はスレッド プールで
スレッド プール
タスク1
タスク2
…
※ I/O completion port
あるスレッドアプリ
I/O完了ポート
ハードウェア
I/O開始 I/O完了
コールバック
登録
コールバック登録後、
すぐにスレッド上での
処理を終了
割り込み信号
I/O完了ポート※
• スレッドを確保せずI/Oを待つ仕組み
• コールバックを登録して、割り込みを待つ
• コールバック処理はスレッド プールで
スレッド プール
タスク1
タスク2
…
※ I/O completion port
あるスレッドアプリ
I/O完了ポート
ハードウェア
I/O開始 I/O完了
コールバック
登録
コールバック登録後、
すぐにスレッド上での
処理を終了
割り込み信号
ポイント
• I/O-boundな処理にスレッドを使っちゃダメ
• I/O用の非同期メソッドが用意されてる
(内部的にI/O完了ポートを利用)
問題: スレッド安全保証
• スレッド安全なコードは高コスト
• いっそ、単一スレッド動作を前提に
• メッセージ ポンプ
MSG msg;
while (PeekMessage(&msg, (HWND)NULL, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
} こんな感じのループが単一スレッドで動作
他のスレッドからメッセージを
キュー越しに受け取って処理
典型例: UIスレッド
• GUIはシングル スレッド動作(UIスレッド)†
• ユーザーからの入力受け付け
• 画面の更新
UIスレッド
ユーザー
からの入力
OK
グラフィック
更新
他のスレッド
処理中は
応答不可
他のスレッドから
は更新不可
† C#/.NETに限らず、だいたいの言語・環境でGUIはシングル スレッド動作
矛盾
単一スレッドからしか
UI更新できない
そのスレッドを止める
とUIフリーズ
シングル スレッド推奨
マルチ スレッド推奨
OK
解決策: メッセージ配送
1. スレッド プールで重たい処理
2. UIスレッドに処理を戻してからUI更新
UIスレッド
OK
更新
他のスレッド
重たい処理
Dispatcher.Invoke
Task.Run
※ dispatcher: 配送者
渡す役割を担うのが
ディスパッチャー※
他のGUIフレームワークだとevent queueとかhandlerとかいう
名前で提供されたりするものの、やってることは一緒
いったんUIスレッド
にメッセージを渡す
おまけ: 他の言語
• スクリプト言語の類はだいたいスレッド機能
持ってない
• GUIを前提にしていないか
• GUIがフリーズするほどの処理を書くことを前提に
していないか
• C++とかは最近(11で)ようやく標準化
• それまでは、スレッドは結構環境依存
ポイント
• 応答性と全体のパフォーマンスとの兼ね合い
• スレッド プール
• I/O待ちのためにスレッドは使っちゃダメ
• I/O完了ポート
• スレッド安全保証はそれなりに高コスト
• シングル スレッドでメッセージ ポンプ
• メッセージ配送
Task
クラス
2時間目
• メタデータ
• 動的リンク
• JIT
• PCL
1
2
3
メタデータとは
• 実行可能ファイル中には本来不要なデータ
• プログラムを作るためのデータ
• 実行に必要なデータよりもメタ(高次)
var x = p.X;
var y = p.Y;
var z = p.Z;
var pp = (byte*)&p;
var x = *((int*)pp);
var y = *((int*)(pp + 4));
var z = *((int*)(pp + 8));
プロパティ名とか
単にプログラムを動かすだけなら
相対アドレスだけわかればいい
.NETでは、こういうメタデータを実行可能ファイルに残す
.NETのメタデータ
• .NETのメタデータ(≒ 型情報)
• DLLにどういう型が含まれるか
• どういう型がどういうメンバーを持っているか
• 外部のどういうDLLを参照しているか
• バージョン情報
• DLLにメタデータを残すことで
• プログラミング言語をまたげる
• 動的リンクでのバージョン管理ができる
おまけ: 昔と今
• COMの頃
• メタデータを自分で書いてた
(自動化するツールあるけど)
• .tlb/.olbファイル
• .NET Framework
• .NET自体がメタデータの規格を持ってる
• C#とかをコンパイルするだけで作られる
おまけ: C++/CX
• C++ Component Extensions
• マイクロソフトのC++拡張
C++/CX
素の標準
C++コード
COMコード
メタデータ
(winmd)
コンパイル
C++内で完結して使う分
にはオーバーヘッドなし
COMを呼べる言語なら
何からでも呼べる
.NETのメタデータと互換
.NETから簡単に呼べる
動的リンク
実行可能ファイルにメタデータが含まれていることで
ライブラリ共有
• 前提: ライブラリは共有する
アプリA
アプリB
ライブラリX
ライブラリY
version 1
version 1
動的リンク
• ライブラリ単体での差し替え
• セキュリティ ホールや、致命的なバグの修正
アプリA
アプリB
ライブラリX
ライブラリY
version 2
version 1
更新不要
差し替え
アプリ実行時にライブラリをリンクしなおしてる = 動的リンク
いまどきの事情
• ライブラリはアプリのパッケージに同梱
• 動的なリンクいるの?
アプリA ライブラリX
ライブラリY アプリB
ライブラリX
ライブラリY
アプリAパッケージ アプリBパッケージ
別バイナリ・別々に配布
別バージョンでも困らない
とはいえ…
差分ダウンロード
※ Windowsストア アプリはこういう仕組み持ってる
アプリA ライブラリX
ライブラリY
アプリAパッケージ version 1
version 1
アプリA ライブラリX
ライブラリY
アプリAパッケージ version 2
version 2
version 1 version 1
version 1 version 1
ライブラリX
差分
version 2
ダウンロード
アプリA ver.1
インストール機
バージョンアップ時
更新
ここだけ新しい
アプリB
ライブラリX
ライブラリY
アプリ間でライブラリ共有
※ Windowsストア アプリはこういう仕組み持って
アプリA ライブラリX
ライブラリY
アプリAパッケージ
ライブラリY
アプリBパッケージ
差分
アプリA
インストール機アプリB
アプリBインストール時
X、Yは同じものを共有
(ハード リンク作るだけ)
version 1
version 1 version 1
version 1
同じバージョン
パッケージ管理
※ ASP.NET vNextはこういう仕組み持ってる
開発機 アプリ サーバー パッケージ リポジトリ
例
http://nuget.org
http://myget.org
project.json
*.cs
"dependencies": {
"Library.A": "",
"Library.B": ""
}
var c = new Configuration();
c.AddJsonFile("config.json");
a.UseServices(services => …);
ソースコードだけを
アップロード
サーバー上で
編集可能
ライブラリの不足・更新
はクラウドから取得
ポイント
• 動的にリンク
• 部分更新・差分ダウンロード
• ライブラリ共有
• パッケージ管理
• リンクに必要な情報(メタデータ)を実行可能
ファイルに残す
• メタデータも規格化されてる
• C#/.NETの場合はコンパイルするだけで作られる
JIT
(Just-in-Time compile)
実際の動的リンクの挙動
中間コードとJITコンパイル
• .NET Frameworkの中間言語
• 高級言語のコンパイラーを作る人と、CPUごとの最
適化する人の分業化
• セキュリティ チェックしやすくなったり
• 動的リンク時に、コード修正の影響をJITで吸収
高級言語
(C#など)
中間言語
(IL)
ネイティブ
コード
ビルド時に
コンパイル
Just-in-Time
コンパイル
JITである必要ない
LLVMとかでも中間言語介してコンパイルしてる
ストア審査ではじくとか他にも手段はある
例
• (C#で)こんな型があったとして
• 整数のフィールドを3つ持つ
public struct Point
{
public int X;
public int Y;
public int Z;
}
例
• こんなメソッドを書いたとする
• フィールドの掛け算
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 ebp
mov ebp,esp
cmp dword ptr ds:[5011058h],0
je 00FE2A01
call 74B7AEA8
mov eax,dword ptr [ebp+8]
lea edx,[ebp+8]
imul eax,dword ptr [edx+4]
lea edx,[ebp+8]
imul eax,dword ptr [edx+8]
pop ebp
ret 0Ch
4とか8とかの数値に
型情報は残らない
メモリ レイアウト
• この4とか8の意味
public struct Point
{
public int X;
public int Y;
public int Z;
}
Point
X
Y
Z
4バイト
8バイト
※レイアウトがどうなるかは環境依存
メモリ レイアウト
• この4とか8の意味
public struct Point
{
public int X;
public int Y;
public int Z;
}
Point
X
Y
Z
4バイト
8バイト
※レイアウトがどうなるかは環境依存
ILの時点までは名前
で参照してる
ネイティブ コードは
レイアウトを見て
数値で参照してる
数値でのフィールド参照
• 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)
変更してみる
• 大して影響しなさそうな
ほんの些細な変更をしてみる
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
X
Y
Z
Point
X
Z
Y
※ この例(フィールド変更)以外でも、仮想メソッド テーブル
とかいろいろレイアウトが変わるものがある
ILレベルでの影響
• 影響なし
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が吸収してくれる
ネイティブ レベルでの影響
• ここで影響が出る
push ebp
mov ebp,esp
cmp dword ptr ds:[5011058h],0
je 00FE2A01
call 74B7AEA8
mov eax,dword ptr [ebp+8]
lea edx,[ebp+8]
imul eax,dword ptr [edx+4]
lea edx,[ebp+8]
imul eax,dword ptr [edx+8]
pop ebp
ret 0Ch
8
4
更新が必要
利用側の再コンパイルが必要
ライブラリ側だけの差し替えじゃダメ
ただし…
• この役割に焦点を当てるなら…
• 毎回毎回JITする必要ない
• 全部が全部ILな必要ない
Ngen
• Ngen.exe
• Native Image Generator
• ILを事前にネイティブ化するためのツール
• 自前管理が必要
• アプリのインストーラー※とかを作って明示的に呼び出し
• 参照しているライブラリが更新された時には呼びなおす
必要あり
• かなり面倒なのでアプリを
Ngenすることはめったにない
• .NET自体が標準ライブラリの
高速化のために使ってる
※ 要するに、JITの負担を起動時じゃなくてインストール時に前倒しする
Auto-Ngen
• .NET Framework 4.5以降なら
• NgenがWindowsサービスとして常に動いてる
• アイドル時に動作
• 利用頻度の高いものを自動的にNgen
• デスクトップ アプリの場合はGACアセンブリのみ
• Windowsストア アプリの場合はすべてのアセンブリ
• よく使うアプリの起動はだいぶ早くなる
• インストール直後の起動は相変わらず遅い
MDIL (ネイティブのおさらい)
• おさらい: ネイティブ コードだと
push ebp
mov ebp,esp
cmp dword ptr ds:[5011058h],0
je 00FE2A01
call 74B7AEA8
mov eax,dword ptr [ebp+8]
lea edx,[ebp+8]
imul eax,dword ptr [edx+4]
lea edx,[ebp+8]
imul eax,dword ptr [edx+8]
pop ebp
ret 0Ch
参照しているライブラリ
のレイアウトが変わった
時に再コンパイルが必要
MDIL (部分的にネイティブ化)
• じゃあ、こんな形式があればいいんじゃ?
push ebp
mov ebp,esp
cmp dword ptr ds:[5011058h],0
je 00FE2A01
call 74B7AEA8
mov eax,dword ptr [ebp+8]
lea edx,[ebp+8]
imul eax,dword ptr [edx+4]
lea edx,[ebp+8]
imul eax,dword ptr [edx+8]
pop ebp
ret 0Ch
int32 Point::X
int32 Point::Y
int32 Point::Z
ほぼネイティブ レイアウトのところ
だけ抽象的に型情報
を残しておく
MDIL: Machine Dependent Intermediate Language
MDIL (Compile in the Cloud)
• ストア サーバー上でMDIL化までやっておく
C#コード
IL
MDIL
ネイティブ コード
C#コンパイラー
MDILコンパイラー
リンカー
開発環境でILにコンパイル
Windowsストア サーバー
上でILをMDIL化
Windows Phone実機上では
レイアウトの解決(リンク)だけ行う
インストール直後の起動も高速
※ Windows Phoneアプリはこういう仕組み持ってる
ポイント
• JITの主な利点
• 動的リンクしやすい
• (後述の、動的コード生成しやすいとかもある)
• ただし、常にJust-In-Timeな必要ない
• インストール時 → Ngen
• サービスで定期的に → Auto-Ngen
• ストア サーバー上で → Compile in the Cloud
vNextはJITだけじゃない
• (詳細は後述)
選べる実行形態
• 事前に完全ネイティブ化 → .NET Native
• ソースコード配置 → Cloud Mode
リフレクション
動的コード生成
メタデータ利用
• メタデータ(プログラム生成に必要な情報)を
持っているということは
• 動的にプログラム コードを生成できる
• 自己反映的動作(リフレクション)
コード生成API
高級言語
(C#)
構文木
IL
ネイティブ
コード
parse
emit
JIT
.NET Compiler Service†
式ツリー
(System.Linq.Expressions)
ILGenerator
† 旧称(コードネーム)Roslyn
動的なコンパイルが可能
C#コードから
• .NET Compiler Serviceの例
session.Execute<Func<Point, int>>("p => p.X * p.Y * p.Z")
C#ソースコードをコンパイル
C#→[parse]→構文木→[emit]→IL→[JIT]→Native