C# と .NET の基本 の続きです。.NET の内部で何が起きているかを掘り下げます。
C# のコンパイル結果は、すぐに機械語になるわけではありません。一度中間言語(IL: Intermediate Language) と呼ばれる命令セットに変換されます。
中間言語が存在する主な理由はプラットフォームの違いを吸収するためです。Windows・macOS・Linux・iOS・Android はそれぞれ異なる機械語(CPU の命令セット)を持ちます。
C# を直接機械語にコンパイルすると、プラットフォームごとに別々のコンパイルが必要になります。中間言語を挟むことで、C# の変換は1回で済み、各プラットフォームへの機械語への変換はランタイム側に任せられます。
flowchart TD
csharp["C# ソースコード"]
il["中間言語 (IL)"]
win["Windows 向けネイティブコード"]
mac["macOS 向けネイティブコード"]
ios["iOS 向けネイティブコード"]
csharp -->|"コンパイル(1回)"| il
il --> win
il --> mac
il --> ios
.NET ランタイムの中核は CLR(Common Language Runtime) です。CLR が IL を受け取り、実行するハードウェアに合った機械語に変換して実行します。
この変換は実行する直前に行われます。これを JIT(Just-In-Time)コンパイルと呼びます。「ちょうど間に合うときに(実行直前に)コンパイルする」という意味です。
flowchart LR
src["C# ソースコード"]
il["中間言語 (IL)"]
jit["JIT コンパイル (CLR)"]
native["ネイティブコード (機械語)"]
src -->|"Roslyn コンパイラー(ビルド時)"| il
il -->|"実行時"| jit
jit --> native
JIT の特徴:
| 項目 | 内容 |
|---|---|
| 変換タイミング | 実行時(プログラム起動後に変換) |
| メリット | 実行環境の CPU に合わせた最適化が可能・繰り返し実行されるコードは高速化される |
| デメリット | 初回起動時にコンパイルコストがかかる |
Unity には2つの .NET 実行方式があります。
Unity エディター上での実行や、スタンドアロン PC ビルドで使われます。CLR の代わりに Mono というオープンソースの .NET ランタイムを使いますが、JIT の仕組みは同じです。実行が速く始められるため、開発中の動作確認に適しています。
iOS・Android・ゲームコンソールなど多くのビルドターゲットで使われます。IL2CPP(IL to C++) は IL をいったん C++ コードに変換し、さらにネイティブコードにコンパイルします。
flowchart LR
src["C# ソースコード"]
il["中間言語 (IL)"]
cpp["C++ コード"]
native["ネイティブコード"]
src -->|"コンパイル"| il
il -->|"IL2CPP 変換"| cpp
cpp -->|"C++ コンパイラー"| native
すべての変換をビルド時に事前に行う方式を AOT(Ahead-Of-Time)コンパイルと呼びます。
JIT vs AOT の比較:
| 比較項目 | JIT(Mono) | AOT(IL2CPP) |
|---|---|---|
| 変換タイミング | 実行時 | ビルド時 |
| 起動速度 | やや遅い | 速い |
| 最高実行速度 | 動的な最適化が効く | コンパイル時最適化済み |
| ビルド時間 | 短い | 長い |
| 主な用途 | エディター・PC スタンドアロン | iOS・Android・コンソール |
Unity のスクリプトが「エディターでは動くがビルドでは動かない」場合、IL2CPP の制約が原因であることがあります。IL2CPP はビルド時にコードを解析するため、リフレクション(実行時に型情報を動的に取得する機能)や一部のジェネリクスが制限される場合があります。本格的なプロジェクトでは早い段階でビルドテストを行う習慣をつけると良いでしょう。