【.NET】ジェネリック共有について【Unity】
はじめに
ご存じのとおり .NET にはジェネリックがあります。
これは Java のような型消去方式と C++ のような全展開方式の中間的な仕組みです。参照型に限定すれば、Java と同様に型消去が行われます。
一方で、コンパイルコストやバイナリサイズを抑える代わりに、実行性能は犠牲になります。
共有実装 | 個別実装 | |
---|---|---|
コンパイルコスト | ⭕️ | ✖ |
バイナリサイズ | ⭕️ | ✖ |
実行速度 | △ | ⭕️ |
結果として「複雑だが高速」という実装があったとき、その効果が値型でしか発揮されない場合があります。
参考例1 ZLinq / 参考例2 筆者によるTakeLast最適化PR
ここでは .NET と IL2CPP におけるジェネリック共有について簡単に紹介します。
.NET
まず基本として MethodTable を知っておく必要があります。
MethodTable という名前ですが、実質的には型情報そのものです。参照型の先頭に SyncBlock(hashcode, lock, 一時的 GC 情報など)とともに存在しており、参照型が指すポインタは MethodTable**
になっています。
System.__Canon
ジェネリック共有に関連してよく登場するのが System.__Canon です。
これは任意の参照型を表すプレースホルダーとして用いられます。
内部メソッドテーブルで、ジェネリックインスタンス化用の「正準」メソッドテーブルをインスタンス化するために使用されます。
「__Canon」という名前はユーザーには表示されませんが、デバッガのスタックトレースには頻出するため、短い名前が付けられています。
「canonical/正準」という言葉は耳慣れないかもしれませんが、ここでは「すべての参照型の代表」と理解すればよいでしょう。
具体的な共有実装については公式ドキュメントを参照してください。
筆者が公開している GPT5による翻訳版 / 原文
後日、WinDbg を用いた実際の処理追跡についても記事にする予定です。
IL2CPP
IL2CPP では以前から「参照型」と「整数型(または列挙型)」に対してジェネリック共有が実装されていました。
さらに Unity 2022.1 beta からは参照型に限定されず、あらゆる型に対応できるようになりました。これにより動的コード生成を行わずにあらゆるジェネリックを利用可能になりました。
どうやって実現しているか
.NET の GC は正確に参照を把握して管理しますが、Unity の Boehm GC は保守的 GC であり「参照らしき値」があれば解放しません。そのため参照と値を厳密に区別する必要がなく、実装上の自由度が高いのです。
そして stackalloc
を利用してスタック上にプレースホルダーを確保し、これをジェネリックなローカル変数領域として利用することで、任意サイズの型に対応できます。
ただし、参照型専用実装よりオーバーヘッドが大きくなるため、パフォーマンス重視の場合は smaller build を避け、必要最小限にとどめるのが望ましいでしょう。
CoreCLR への移行状況
Unity は現在(2025/08/20)、CoreCLR への移行を進めています。ただし GC 対応が難しく、2021 年時点では .NET に「保守的 GC モード」を要求していた経緯があります。
また IL2CPP に関しては、2023 年時点で CoreCLRGCへの移行予定はなく、Full Generic Sharing が廃止されることもなさそうです。
まとめ
.NET と Unity(IL2CPP)におけるジェネリック共有は、「コンパイル効率・バイナリサイズ」と「実行性能」 のトレードオフの上に成り立っています。.NET では参照型にのみ対応、Unity IL2CPPでは GC の違いを利用してより広い型に対応しています。
今後 Unity は CoreCLR へ移行を進めていますが、IL2CPP の Full Generic Sharing は存続する見込みです。つまり、両者のアプローチを理解しておくことは、これからの .NET/Unity 開発においても有用だといえるでしょう。
Discussion