
- Unity Blog: 「Reducing GC Allocations in Unity」
- Stack Overflow の Mono vs. IL2CPP パフォーマンスに関する議論
---
**結論:**
Mono がメモリと実行を管理する仕組みを理解し、効果的にプロファイルしてターゲット最適化を施すことで、Unity における C# スクリプトのランタイムオーバーヘッドを大幅に削減できます。](/_next/image?url=%2Fscreenshots%2F2025-12-29%2F1766967957532.webp&w=3840&q=75)
2025/12/29 6:41
## Unity の Mono に関する問題 **C# コードが想定よりも遅く動作する理由** --- ### 1. 背景 - Unity は C# スクリプトの実行に **Mono**(または IL2CPP)をランタイムとして使用しています。 - 開発者は、ネイティブ C++ コードと比べてパフォーマンスが低下することに気づくことが多いです。 ### 2. 遅延の一般的な原因 | カテゴリ | よくある問題 | 発生理由 | |----------|--------------|----------| | **ガベージコレクション (GC)** | ゲームプレイ中に頻繁にメモリ確保 | GC の停止がゲームスレッドを止め、フレームレートの乱れを引き起こします。 | | **Boxing/Unboxing** | 値型をオブジェクトへキャスト | 一時的なヒープオブジェクトが生成され、収集対象になります。 | | **リフレクション** | 実行時に `System.Reflection` を使用 | 動的型解決のため、リフレクションは遅いです。 | | **文字列連結** | ループ内で `+` を繰り返し使用 | 多くの中間文字列が生成され、GC の負荷が増大します。 | | **大型 MonoBehaviour** | 一つのスクリプトに多くの責務を持たせる | フレームごとの作業量が増え、キャッシュミスにつながります。 | ### 3. プロファイリングのヒント 1. **Unity Profiler → CPU Usage を開く** - 「Managed」と「Native」の時間差に注目します。 2. **Memory タブを使用** - ゲームプレイ中に急増する割り当てを探ります。 3. **Profiler: Mono Runtime を有効化** - GC、JIT、メソッド呼び出しの詳細が確認できます。 ### 4. 最適化戦略 - **割り当てを最小限に抑える** - オブジェクトを再利用;頻繁に使うインスタンスはプールします。 - ループ内で文字列を作る場合は `StringBuilder` を使用。 - **Boxing を避ける** - 値型はそのまま保持し、`object` へのキャストは控えます。 - **リフレクション結果をキャッシュ** - 最初の検索後に `MethodInfo` や `FieldInfo` を保存します。 - **MonoBehaviour の複雑さを減らす** - 大きなスクリプトは機能ごとに分割し、専念型コンポーネントへ移行。 - **ホットパスにはネイティブプラグインを使用** - 性能重視のコードは C++ プラグインへオフロードします。 ### 5. ベストプラクティス | 実践 | 実装例 | |------|--------| | **早期にプロファイル** | 開発初期から頻繁にプロファイラを走らせます。 | | **クリーンコードを書く** | 可読性重視だが、割り当てには注意します。 | | **Update ループは軽量化** | 重いロジックは Coroutine やバックグラウンドスレッドへ移行可能です。 | ### 6. リソース - Unity Manual: [Performance Profiling](https://docs.unity3d.com/Manual/Profiler.html) - Unity Blog: 「Reducing GC Allocations in Unity」 - Stack Overflow の Mono vs. IL2CPP パフォーマンスに関する議論 --- **結論:** Mono がメモリと実行を管理する仕組みを理解し、効果的にプロファイルしてターゲット最適化を施すことで、Unity における C# スクリプトのランタイムオーバーヘッドを大幅に削減できます。
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
Unity の現在の Mono ランタイムは、モダンな .NET と比べて約 2–3 倍遅く、同一ハードウェア上で実行するとベンチマークで最大 ~15 倍の速度向上が確認されています。このギャップは、Mono の JIT コンパイラが高度に最適化されていないアセンブリを生成する一方、.NET の JIT がスカラー化やレジスタベース演算などの高度な最適化を行うためです。
2006 年に導入以来、Mono は Unity のデフォルト C# ランタイムでした。Microsoft は 2014 年に .NET Core をオープンソース化し、2016 年 6 月にクロスプラットフォームサポートをリリースしました。2018 年、Unity はエンジンを Microsoft の CoreCLR(.NET Core 背後の CLR)へ移植する計画を発表し、パフォーマンス向上とプラットフォーム間の差異を縮小するとともに、一部ワークロードで 2–5 倍のブーストが期待できるとしました。
主なベンチマーク結果は次の通りです:
- Mono ベースのエディタ起動時間:約 100 秒
- 同等の .NET 単体テスト:約 38 秒
- リリースモードスタンドアロンビルド:Mono 約 30 秒、.NET 約 12 秒
- 4k×4k マップ生成:.NET 約 3 秒
- int.MaxValue イテレーションの緊密ループテスト:Mono 約 11.5 秒、.NET 約 0.75 秒(約 15 倍遅い)
- デバッグモード同じループ:約 67 秒(追加チェックが原因)
モダンな .NET の JIT は小さな値型をスカラー化し、不変計算をループ外に持ち出し、レジスタベース演算を使用するなど、Mono が適用できない最適化を実行します。CoreCLR は Span、ハードウェアイントリンシック、SIMD パスといった高度な機能も公開し、特定のコード(例:シンプルノイズ)でパフォーマンスが倍増する可能性があります。
Unity の Burst コンパイラは選択された C# メソッドを LLVM 生成ネイティブアセンブリに変換できますが、適用範囲が限定されています。CoreCLR の JIT はこれらの制約なしで同等かそれ以上の性能を提供できる可能性があります。
CoreCLR への移行は Unity 6.x を対象としており、本番稼働準備は 2026 年またはそれ以降になる予定です。採用されれば、開発者は高速なエディタ起動、短縮されたビルド時間、および Just‑In‑Time コンパイルを許可するプラットフォーム上でより効率的なランタイムコードを体験できます。ただし、Ahead‑Of‑Time (AOT) コンパイルが必要なデバイスは引き続き IL2CPP に依存するため、性能向上はターゲットプラットフォームによって異なる可能性があります。
本文
Unity の Mono ランタイムで C# コードを実行する速度は、今日の基準では非常に遅い――期待よりも遥かに遅くなることが多いです。
私たちのゲームは、Unity が使う Mono と比べて、モダンな .NET で 2〜3 倍速く動作します。さらに数件の小さなベンチマークでは、最大で 15 倍まで高速化が確認できました。本稿ではその実証結果を提示し、Unity の .NET モダナイズができるだけ早く本番環境へ投入されるべき理由を解説します。
ここに至った経緯
- 2006 年 – Unity は Mono フレームワークで C# プログラムを実行していました。これは .NET のマルチプラットフォーム実装の中では唯一実用的なものでした。
- Mono はオープンソース で、Unity がゲーム開発向けに調整できるようになっていました。
その後約十年が経過し、Microsoft は .NET(特に .NET Core)をオープンソース化しました。2016 年 6 月には公式のクロスプラットフォームサポート付きで .NET Core 1.0 がリリースされました。その以降、Roslyn コンパイラ・プラットフォーム、新しい JIT、性能向上、新機能が次々に登場し、エコシステムは勢いを増しました。
2018 年、Unity のエンジニアたちは CoreCLR(マルチプラットフォーム Common Language Runtime)への移植を検討しました。主な動機は性能と統一感でした:
「CoreCLR は Unity のゲーム開発者にとって非常に有益です。Mono ランタイムと比べて 2〜5 倍、あるいは特定のワークロードでは最大で 10 倍程度の性能向上が期待できます!」
残念ながら、2025 年末現在でも CoreCLR 上でゲームを動かすことはできません。
性能ギャップ
Mono と .NET の差を語るには、Unity で書いたゲームをモダンな .NET で実行することが不可能なため、直接比較は出来ません。しかし、Unity に依存しないコードの性能は対比できます。
私たちのゲームは シミュレーション(ビジネスロジック)とレンダリングを厳密に分離 しているため、任意の .NET バージョンでコンパイル・実行可能です。
デバッグモード
マップ生成をデバッグするユニットテストを書きました。Unity は DLL を再コンパイルしドメインをリロードするだけで 15 秒以上かかりますが、テスト自体は 40 秒で完了 ― Mono より 3 倍以上速いです。更に深掘りしました。
| ランタイム | モード | 実行時間 |
|---|---|---|
| Unity (Mono) | デバッグ | 100 秒 |
| .NET ユニットテスト | デバッグ | 38 秒 |
図 1 はプロファイラトレースで、保存ファイルの読み込み・マップ生成・シミュレーション初期化にかかる時間を示しています。Unity/Mono では 100 秒、.NET では 38 秒です。
リリースモード(スタンドアロン実行ファイル)
リリースビルドでも Mono は遅いままです。最適化されたスタンドアロン Unity 実行ファイルはエディタを 3 倍以上速くします。同じコードを .NET のリリースモードで走らせると:
| ランタイム | モード | 実行時間 |
|---|---|---|
| Unity (Mono) | リリース | 30 秒 |
| .NET ユニットテスト | リリース | 12 秒 |
図 2 は 12 秒の実行時プロファイラトレースです。
その 12 秒で、4k×4k のマップをすべて利用可能なスレッドと数百個の複合ノイズ関数で約 3 秒で生成します。図 3 はこのトレースを拡大したものです(青枠は実際にユニットテストがゲームをステップさせる部分)。
Mono と .NET の JIT が生成する x86 アセンブリを見るには、記事末の Extras セクションをご覧ください。
結論
Mono は性能面で .NET を大きく下回っています。差はランタイム最適化と非最適化アセンブリを生成する JIT の劣った設計に起因します。ほとんどのプロジェクトでは 1.5〜3 倍程度の速度向上が期待できます。
ゲーム開発者、あるいはプレイヤーであれば、CoreCLR がゲームやエディタ性能を大幅に押し上げる理由が見えてくるでしょう。過去八年間、Unity のリーダーシップは他事業に注力し、.NET モダナイズを後回しにしてきました。
なぜ重要か
- 新しい言語機能:C# は便利な構文を追加しますが、本当のメリットは新 JIT が多倍速アップをもたらす点です。
- 制限のない API:Span、ハードウェアイントリンシック、最新の SIMD パスなどが CoreCLR で利用可能になり、例えば私たちのシンプルノイズジェネレータの性能を倍増させる可能性があります。
- Burst と CoreCLR の比較:Unity の Burst コンパイラは LLVM を通じてマークされた C# メソッドを最適化されたネイティブアセンブリへ変換しますが、制限が厳しいです。モダンな JIT(CoreCLR)はこれらの制約なしに Burst の性能と同等または上回ることが可能です。
- 事前コンパイル(AOT):スタートアップ時間を短縮し、JIT が制限されるプラットフォーム(例:iOS)で不可欠です。Unity は現在 IL2CPP を使用していますが、CoreCLR AOT は計画されていません。IL2CPP は独立した技術です。
要するに、CoreCLR は Unity のゲームのすべてのボトルネックを自動的に解決するわけではありません。しかし、多くのコード生成上の非効率性を排除し、高性能なマネージドコードを可能にします。上述したベンチマークは、モダン .NET がより少ない CPU サイクルで多くの作業を実行できることを示しています ― 現在 Unity ユーザーが享受できていないメリットです。
Unity が本番レベルの CoreCLR を提供できれば、「新しい C#」だけではなく、ランタイム性能の向上、イテレーション時間の短縮、パフォーマンス余裕の拡大、ドメインリロード不要化、GC の改善、さらにはネイティブコードの削減も実現します。そうでなければ、管理コードに依存するすべての Unity プロジェクトに対して見えない税金が残ります。
Unity 開発者の皆さんを応援しています ― CoreCLR が勝利です!
技術的深掘り:Mono と .NET のアセンブリ比較
以下は、カスタム構造体を足し合わせる単純なループをテストするコードです。
static class Program { static void Main() => Console.WriteLine(RunTest(int.MaxValue)); public static TestStruct RunTest(int iterations) { var value1 = new TestStruct(iterations % 2); var value2 = new TestStruct(iterations % 7); var value3 = new TestStruct(iterations % 13); TestStruct result = default; for (int i = 0; i < iterations; ++i) { result += value1 + value2; result += value1 + value3; } return result; } } readonly struct TestStruct { public readonly int Value; public TestStruct(int value) => Value = value; public static TestStruct operator +(TestStruct lhs, TestStruct rhs) => new TestStruct(lhs.Value + rhs.Value); public override string ToString() => Value.ToString(); }
リリースモードでコンパイルし、スタンドアロン実行ファイルとして走らせた結果:
-
.NET (x64):JIT が不変演算(
、a = value1 + value2
)をループ外へ持ち上げ、レジスタ操作のみで処理します。b = value1 + value3add r8d, edx add edx, r10d …実行時間:
繰り返しで約 750 ms。int.MaxValue -
Mono (x64):JIT がインライン化・持ち上げを失敗し、メモリ間で値を移動させる多くの
命令が発生します。movmovsxd rax,dword ptr [rsp+0C0h] … add eax, ecx …実行時間:約 11 500 ms(約 15 倍遅い)。
-
Mono (Debug in Unity Editor):さらに悪化し、約 67 秒。追加チェックとシーケンスポイントオーバーヘッドが実行時間を膨らませます。
結論
モダン .NET の JIT は小さな値型をスカラー化し、不変作業を持ち上げることでホットループをレジスタ演算数に減らします。対照的に Mono はメモリ間で値をシフトするため、シミュレーション中心のコードでは実質的な遅延が発生します。