Advertisement

C#とILとネイティブと

Microsoft MVP
Dec. 21, 2013
Advertisement

More Related Content

Slideshows for you(20)

Advertisement

Recently uploaded(20)

Advertisement

C#とILとネイティブと

  1. C#とILとネイティブと 岩永 信之
  2. 本日の内容 • C#コードのコンパイル過程 • どうしてIL (.NETの中間言語)なのか • ライブラリのコード変更の影響 • ソースコード配付でない理由 • ネイティブ コード配付でない理由 • ネイティブ コード化 • Ngen、MDIL、Project “N”
  3. C#、ILの コンパイルの過程 C# → IL → ネイティブ
  4. コンパイル C# コード C#コンパイラー IL JITコンパイラー ネイティブ コード
  5. コンパイル .NET言語 C#の他にも • Visual Basic C#コンパイラー • F# • … C# コード IL static int GetVolume(Point p) JITコンパイラー { return p.X * p.Y * p.Z; } ネイティブ コード
  6. コンパイル 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
  7. コンパイル 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
  8. 例 • (C#で)こんな型があったとして public struct Point { public int X; public int Y; public int Z; } • 整数のフィールドを3つ持つ
  9. 例 • こんなメソッドを書いたとする static int GetVolume(Point p) { return p.X * p.Y * p.Z; } • フィールドの掛け算
  10. 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 }
  11. ネイティブ コード • 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とかの数値に 型情報は残らない
  12. メモリ レイアウト • この4とか8の意味 Point X public struct Point { public int X; public int Y; public int Z; } Y 4バイト 8バイト Z ※レイアウトがどうなるかは環境依存
  13. メモリ レイアウト • この4とか8の意味 ILの時点までは名前 で参照してる public struct Point Point X { public int X; public int Y; public int Z; } Y 4バイト 8バイト ネイティブ コードは レイアウトを見て Z 数値で参照してる ※レイアウトがどうなるかは環境依存
  14. 数値でのフィールド参照 • 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)
  15. ここまでまとめ • メモリ レイアウト C# コード IL ネイティブ コード ILまでは型情報を残している フィールド名で値を参照 ネイティブ コードになると型情報が残らない レイアウト上のオフセット数値で値を参照
  16. おまけ: ILの見方 • ildasm.exe • Program Files以下 • Microsoft SDKsWindows[Windowsのバージョ ン]binNETFX[.NETのバージョン]
  17. おまけ: Nativeの見方 • Visual Studioのメニューから • デバッグ実行時に • [デバッグ] → [ウィンドウ] → [逆アセンブル] • Ctrl+Alt+D
  18. コード変更の影響
  19. 依存関係 • 開発体制としてありがちな状況 他社製 ライブラリ 自社製 ライブラリ アプリ
  20. 依存関係 • もちろん、実際はもっと複雑な依存関係が • 多対多 • 多段 • 開発者と利用者は別 • • • • 別人 別チーム 別会社 他国
  21. 今回の例でいうと • 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メソッド側に及ぼす影響を考える
  22. 変更してみる • 大して影響しなさそうな ほんの些細な変更をしてみる public struct Point { public int X; public int Y; public int Z; } public struct Point { public int X; public int Z; public int Y; } フィールドの順序変更
  23. その結果起きること • メモリ レイアウトが変わる※ Point Point X Y Z Z ※ X Y この例(フィールド)以外にも、仮想メソッド テーブルとかいろいろ変わる
  24. C#レベルでの影響 • 影響なし • Point構造体がX, Y, Zという名前の3つのintを持って ることには変わりない 利用側(GetVolumeメソッド側)は 変更があったことを知る必要すらない 再コンパイルの必要なし
  25. 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が吸収してくれる
  26. ネイティブ レベルでの影響 • ここで影響が出る 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 利用側の再コンパイルが必要 ライブラリ側だけの差し替えじゃダメ
  27. ここまでまとめ • プログラムのほんの些細な変更によって • メモリ レイアウトが変化する • 利用側(ライブラリ側とは別人が保守)に影響 C#、JITレベルだと影響なし ネイティブ レベルだと影響が出る • .NET製プログラム/ライブラリ をILの状態で配布する 最大の理由 • 事前のネイティブ化が難しい
  28. ソースコード配布 スクリプト言語ならそんな問題こと考えなくていいよ? ただし… 遅い
  29. ネイティブ配布しないとして • ILなんていう中途半端な状態にする必要ある の? • スクリプト言語みたいにソースコード配付すれば? • 理由はパフォーマンス • 測ってみよう
  30. 方法 • C#には動的に(C#プログラム中で)コンパイ ルする仕組みがいくつかあるので • C#コードをコンパイル • 構文木からコード生成 • ILコンパイル この辺りを使って比較
  31. コンパイルの過程 高級言語 (C#) parse 構文木 emit IL JIT ネイティブ コード
  32. コンパイルの過程 高級言語 (C#) parse 構文木 emit IL JIT ネイティブ コード • parse (構文解析、文法説明) • (C#みたいな)自然言語に近い 文法を解析 • コンパイラーが扱いやすい データ形式に変換
  33. コンパイルの過程 高級言語 (C#) parse 構文木 emit IL JIT ネイティブ コード • emit (放射、発行) • コンピューターが解釈できる 命令を出力する • (.NETの場合、CPU命令じゃな くて、ILの仮想マシン命令) • (もちろん言語によっては直接 CPUネイティブ命令を出力)
  34. コンパイルの過程 高級言語 (C#) parse 構文木 emit IL JIT ネイティブ コード • JIT (Just In Time) • プログラムを初めて実行する その瞬間に • IL仮想マシン命令からCPUネ イティブ命令に変換
  35. 動的コンパイル用ライブラリ 高級言語 (C#) Roslyn parse 構文木 式ツリー (System.Linq.Expressions) emit ILGenerator IL JIT ネイティブ コード
  36. 例 • さっきの↓これを生成してみる static int GetVolume(Point p) { return p.X * p.Y * p.Z; }
  37. 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#をスクリプト言語的に実行する機能も持つ
  38. C#コードから • Roslyn ポイントの行: session.Execute<Func<Point, int>>("p => p.X * p.Y * p.Z") C#ソースコードをコンパイル parse→emit→JIT C#→[parse]→構文木→[emit]→IL→[JIT]→Native
  39. 構文木から • 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();
  40. 構文木から • System.Linq.Expressions ポイントの行: Expression.Lambda<Func<Point, int>>( Expression.Multiply( Expression.Multiply( Expression.Field(p, x), Expression.Field(p, y)), Expression.Field(p, z)),
  41. 構文木から • 図で表すと 匿名関数を作る 本体 パラメーター Multiply pから Zを読む p Multiply pから Yを読む pから Xを読む C#→[parse]→構文木→[emit]→IL→[JIT]→Native
  42. 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);
  43. 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);
  44. 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
  45. 実行 • コード生成+呼び出しを1000回ループ かかった時間[ミリ秒] ILGenerator (JIT) 39.89 Expressions (emit→JIT) 67.94 倍遅い 2桁遅い Roslyn 4314 (parse→emit→JIT) ※うちのPCでの計測の場合 ※ 測るたびに1割程度はぶれる
  46. 要するに 高級言語 (C#) parse 構文木 emit IL JIT ネイティブ コード ここがダントツで重い 残りの部分より2桁くらい遅い スクリプト言語だと このコストが丸ごとかかる
  47. 補足: このコストは初回のみ • 何度も同じメソッドを呼ぶ場合 負担はだいぶ小さい GetVolume(p1); GetVolume(p2); GetVolume(p3); GetVolume(p4); ※ JITとか※の処理はここでだけ発生 ネイティブ化済みの コードを実行 C#みたいにIL配布する場合はJITのみ スクリプト言語みたいにソースコード配付する場合はparse→emit→JIT
  48. 補足: このコストは初回のみ とはいえ • JITですら遅いって言われる • どうしてもネイティブと比べられる (比較対象があると結構気になる) • 特に、起動直後に遅くなる (最初が肝心なのに) • これでも、C#は構文的にparseが早い部類 (構文によってはもっと遅い)
  49. ここまでまとめ • やっぱ事前ネイティブ化の要求はある • JITのコストですら嫌われる • 特に、起動直後の遅さ • ましてparseからやるとさらに2桁遅い
  50. 事前ネイティブ化 「やっぱり最初からネイティブにしたい」となったとき に
  51. 選択肢 • 配付形式の選択肢 • ネイティブか • 中間形式(IL)か • ソースコード(スクリプト)か どれがいい? • 状況によるので選びたい • さらに別の選択肢: 部分的にネイティブ化
  52. おまけ: 連携も1つの選択肢 • スクリプト言語との連携 • DLR (Dynamic Language Runtime) • スクリプト言語実装基板 • Iron Pythonなど • スクリプト言語の.NET実装 • ネイティブとの連携 • WinMD (Windows Metadata) • .NETの型情報を.NET以外でも使う • WinRT (Windows Runtime) • .NETからも参照しやすいネイティブ ライブラリ 連携もいいんだけど、C#使いたい…
  53. C#でスクリプティング • ASP.NET • C#ソースコードのままサイトに配置可能 • 初回アクセス時が異様に遅かった※ものの… ASP.NET 4/IIS 7.5からは自動実行に • Azure Websites上のコードならオンライン編集可能 • Visual Studio Online “Monaco”† • Roslyn (新しいC#コンパイラー) • C#をスクリプト的に実行する機能も持つ ※ 初回アクセス時にコンパイルしてた コンパイル(parseから)の遅さは先ほどの例の通り † コードネーム(まだ正式名称じゃない) ブラウザ上で編集できるエディター
  54. C#でネイティブ • どの道、最終的にはネイティブ化(JIT)してるん だから • ネイティブ化自体はそんなに難しくない • Mono ※はAOT†コンパイル機能を持ってる • 問題は「コード変更の影響」で説明した通り • ライブラリ側のレイアウトとかが変わった時の再コ ンパイル Ngen → Auto-Ngen → MDIL → Project “N ” ※ .NET Framework互換のオープンソース実装 † Ahead of Time: ビルドの時点でネイティブ コードにコンパイル
  55. 補足: ネイティブだけど • ここでいう「C#でネイティブ」「.NETアプリ のネイティブ化」とは • IL命令をCPUネイティブ命令に置き替えること • CPUネイティブ命令であってもmanaged • .NETランタイムを参照していて、メモリ操作などは .NET Frameworkの管理下
  56. Ngen • Ngen.exe • Native Image Generator • ILを事前にネイティブ化するためのツール • 自前管理が必要 • アプリのインストーラー※とかを作って明示的に呼び出し • 参照しているライブラリが更新された時には呼びなおす 必要あり • かなり面倒なのでアプリを Ngenすることはめったにない • .NET自体が標準ライブラリの 高速化のために使ってる ※ 要するに、JITの負担を起動時じゃなくてインストール時に前倒しする
  57. Auto-Ngen • .NET Framework 4.5以降なら • NgenがWindowsサービスとして常に動いてる • アイドル時に動作 • 利用頻度の高いものを自動的にNgen • デスクトップ アプリの場合はGACアセンブリのみ • Windowsストア アプリの場合はすべてのアセンブリ • よく使うアプリの起動はだいぶ早くなる • インストール直後の起動は相変わらず遅い
  58. 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
  59. 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
  60. 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実機上では レイアウトの解決(リンク)だけ行う • インストール直後の起動も安心
  61. Project N • コードネーム“Project N”※ • C#/.NETコードを直接(ビルド時に)ネイティブ化す る話も出てるみたい • 詳細はまだ何も語られてない • Auto-NgenとかMDILの流れをくむものっぽい ※ http://www.zdnet.com/microsoft-shows-off-its-next-generation-project-n-compiler-technology-7000023156/
  62. ここまでまとめ • 事前ネイティブ化はそんなに楽じゃない • ライブラリのコード変更の影響を吸収しないといけ ない • かといってアプリ起動のたびにJITしたくない • Auto-Ngen → MDIL → Project N? • アイドル時間を使ってネイティブ化したり • 部分的にネイティブ化したり
  63. まとめ • C# → IL(中間言語) → ネイティブ • ILの意義 • ライブラリのコード変更の影響を吸収 • ソースコード配付よりはだいぶ高速 • ネイティブ コード化 • Auto-Ngen → MDIL → Project N • アイドル時間を使ってネイティブ化したり • 部分的にネイティブ化したり
Advertisement