.NETランタイムとは何か?C#が動くしくみをゼロから図解で解説
初めに
本記事では、C#のコードが実行されるまでの裏側、「.NETランタイム(CLR)」の仕組みをわかりやすく図解で紹介します。
🧠 .NETランタイム(CLR)とは
✅ 定義と役割
.NETランタイム(正式には Common Language Runtime:CLR)は、C# や F#、VB.NET などの .NET対応言語を実行するための仮想実行環境 です。
主な責任:
-
コードの実行(JITコンパイル)
-
メモリ管理(GC)
-
型安全の保証とセキュリティ
-
例外処理とスレッド制御
🧩 C#コードが動くまでのステップ
1. RoslynでC# → IL への変換
C#のソースコードは、まず Roslyn と呼ばれるC#コンパイラによって IL(中間言語) に変換されます。
dotnet build
この時点で生成されるのは .dll や .exe ファイル(PEフォーマット)で、以下の2つを内包します。
要素 | 内容 |
---|---|
ILコード | CPU非依存の中間命令群 |
メタデータ | クラス、型、メソッド、属性などの情報 |
Roslyn(ロズリン)とは?
Roslyn は .NET 用の「オープンソースのコンパイラプラットフォーム」であり、C# や Visual Basic の構文解析・意味解析・コンパイル・リファクタリング・コード生成を可能にします。(C#のコードを読む・理解する・直すための「頭脳」みたいなエンジンです。)
たとえばこんなこと思ったことないですか?
-
Visual Studio で「間違ってるよ!」って赤い波線が出る
-
「この変数の名前、もっとわかりやすくしよう」って自動でリネームされる
-
Ctrl + . を押したら「using を追加しますか?」って出てくる
👉 実はこれ、Roslyn がやってるんです!
2. CLRの読み込み:アセンブリのロード
dotnet run やアプリの起動時、.NETランタイム(CLR)が以下の処理を行います:
-
アセンブリのローディング(AssemblyLoadContext)
-
依存DLLの解決(AssemblyResolve)
-
型解決とバインディング(System.Type, MethodInfoなど)
この時点ではまだネイティブコードにはなっていません。CLRは メソッド単位でJIT対象を検出します。
3. JIT(Just-In-Time)コンパイル:IL → ネイティブ
C#では実行時に JITコンパイラ(RyuJIT)が IL を ネイティブコードに変換します。
JIT(Just-In-Time)コンパイルとは?
JIT(Just-In-Time)コンパイルとは、「実行時にコンパイルを行う方式」のことです。プログラムのコードを実行中にネイティブコード(機械語)に変換して実行する仕組みです。主に**仮想マシン型の言語(例:Java、C#、JavaScriptなど)**で使われます。
メリット
-
高速化
-実行時に最適化を行えるため、純粋なインタプリタよりも高速になります。 -
ランタイム情報を活用した最適化
- 実行中の統計データ(例:どの関数が頻繁に使われるか)を基に最適なコードを生成できます。
🔄 処理の流れ(初回呼び出し時):
-
CLRが実行対象メソッドに到達
-
ILコードをJITコンパイル(例:x64命令に変換)
-
ネイティブコードをメモリにキャッシュ
-
2回目以降はJITなしでネイティブを直接実行
💡 JITの種類
種類 | 特徴 |
---|---|
Pre-JIT | 実行前に全コードをJIT(現実では非推奨) |
Econo-JIT | リソース削減のために一部JIT(モバイル等) |
Normal JIT(現代の標準) | 実行時に必要な部分だけJIT |
4. Tiered Compilation(段階的最適化)in .NET8+
.NET 8では、従来のJITに比べて 「初回から高速、かつ継続的に最適化」 される仕組みが整っています。
その中核にあるのが Tiered Compilation(段階的JIT) です。
🧠 Tiered Compilationとは?
Tiered Compilationは、実行時にメソッドを段階的にJITコンパイルしていく最適化戦略です。(プログラムを最初はサッと動かして、あとから速くしていく仕組みです。)
Tier | 内容 |
---|---|
Tier 0 | 初回呼び出し用の高速かつ簡易的なJIT |
Tier 1 | 一定回数以上呼ばれたメソッドをプロファイル情報をもとに最適化JIT |
これにより、アプリの初回起動は高速になります。
5. 実行と最適化のキャッシュ
JITされたネイティブコードはアプリが終了するまでメモリ上にキャッシュされ、2度目以降の呼び出しは即座に実行されます。
また、.NET 5以降は ReadyToRun(R2R) 形式のプリコンパイルで、事前に一部JITを回避することも可能です。
dotnet publish -c Release -r win-x64 --self-contained true /p:PublishReadyToRun=true
📦 IL→ネイティブの具体例(ILコード一部)
以下は単純なC#メソッドをILに変換した例です:
public static int Add(int x, int y)
{
return x + y;
}
ILDASMで見ると:
.method public hidebysig static int32 Add(int32 x, int32 y) cil managed
{
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldarg.1
IL_0002: add
IL_0003: ret
}
この add 命令が JIT により add eax, ebx のようなネイティブ命令に変換されます。
Discussion