4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DictionaryをIL2CPPでコード生成するとどれくらい肥大化するか

Last updated at Posted at 2025-10-10

お久しぶりです

プロジェクトでメモリ削減の調査をしてるときに
Dictionaryから生成されたコードが肥大化してメモリを圧迫していたので
調査メモとしてまとめました

ざっくり結論

IL2CPP+DictionaryはTKeyとTValueのパターンごとに約400KBのコード生成が行われる
できる限りパターンをそろえるか、NativeHashMapやListや独自実装を使うことで
コード生成を避けましょう

導入

UnityのIL2CPPとは

C#コードをC++コードに変換してバイナリを作る技術のこと
これにより実行速度向上・省メモリ化が見込めます

なぜIL2CPP+Dictionaryが危険なのか

C#はリッチな言語で、実行中に型を決定できます ⇦ JIT (Just-In-Time) コンパイラ
C++は事前にすべての型を決めておく必要があります ⇦ AOT (Ahead-Of-Time) コンパイル

このため、C#のジェネリックをC++のテンプレートに落とし込む際、入りうる型引数ごとにコード生成が行われます
特にDictionaryはクラス自体が高機能なのもあり、テンプレートごとに大量のコードが生成されます

Dictionaryはキャッシュ構造の自動生成に使われることも多く
自動生成で何百個も生成した結果100MBに膨らみRAMを占有してしまいます

MemoryProfilerの「Executable & Mapped」を見ながらコード変更して計測してみるとわかります

今回はどういう定義がどれくらい影響するか見ていき
Dictionaryを使う際の注意点を探していきます

調査

確認環境

Windows 11
Unity 6000.0.49f1

具体的な数値変化

下記フォルダの容量を比較していきます
Library/Bee/artifacts/WinPlayerBuildProgram/il2cppOutput/cpp

数値型の場合

// 元の生成コードサイズ: 691328967 Byte
Dictionary<int, int> sample1 = new(); // ここまでで 3309 増加
Dictionary<int, int> sample2 = new(); // ここまでで 4086 増加
Dictionary<int, int> sample3 = new(); // ここまでで 4863 増加
Dictionary<int, int> sample4 = new(); // ここまでで 5640 増加

同じ型を何度も定義するにはコードはほぼ増えません

// 元の生成コードサイズ: 691328967 Byte
Dictionary<int, int> sample1 = new();    // ここまでで 3309 増加
Dictionary<uint, int> sample2 = new();   // ここまでで 6459 増加
Dictionary<float, int> sample3 = new();  // ここまでで 505503 増加
Dictionary<double, int> sample4 = new(); // ここまでで 825994 増加

// 元の生成コードサイズ: 691328967 Byte
Dictionary<int, int> sample1 = new();    // ここまでで 3309 増加
Dictionary<int, uint> sample2 = new();   // ここまでで 495176 増加
Dictionary<int, float> sample3 = new();  // ここまでで 498455 増加
Dictionary<int, double> sample4 = new(); // ここまでで 826016 増加

// 元の生成コードサイズ: 691328967 Byte
Dictionary<int, int> sample1 = new();       // ここまでで 3309 増加
Dictionary<float, float> sample2 = new();   // ここまでで 498160 増加
Dictionary<uint, uint> sample3 = new();     // ここまでで 820981 増加
Dictionary<double, double> sample4 = new(); // ここまでで 1436033 増加

型をちょっと変えるだけで結構コード量が増えてます
条件によりますが1定義で400KB増えることを覚悟するべきかも

クラスの場合

// 元の生成コードサイズ: 691328967 Byte
Dictionary<TestClass1, TestClass1> sample1 = new(); // ここまでで 6139 増加
Dictionary<TestClass2, TestClass2> sample2 = new(); // ここまでで 11367 増加
Dictionary<TestClass3, TestClass3> sample3 = new(); // ここまでで 16594 増加

// 元の生成コードサイズ: 691328967 Byte
Dictionary<TestClass1, int> sample1 = new();   // ここまでで 6099 増加
Dictionary<TestClass2, float> sample2 = new(); // ここまでで 11563 増加
Dictionary<TestClass3, uint> sample3 = new();  // ここまでで 516079 増加

// 元の生成コードサイズ: 691328967 Byte
Dictionary<int, TestClass1> sample1 = new();   // ここまでで 6099 増加
Dictionary<float, TestClass2> sample2 = new(); // ここまでで 507464 増加
Dictionary<uint, TestClass3> sample3 = new();  // ここまでで 512927 増加

数値型とほぼ同じ

Enumの場合

// 元の生成コードサイズ: 691328967 Byte
Dictionary<Type1, Type1> sample1 = new(); // ここまでで 6054 増加
Dictionary<Type2, Type2> sample2 = new(); // ここまでで 11197 増加
Dictionary<Type3, Type3> sample3 = new(); // ここまでで 16339 増加

// 元の生成コードサイズ: 691328967 Byte
Dictionary<Type1, int> sample1 = new();   // ここまでで 488849 増加
Dictionary<Type2, float> sample2 = new(); // ここまでで 818734 増加
Dictionary<Type3, uint> sample3 = new();  // ここまでで 1432765 増加

// 元の生成コードサイズ: 691328967 Byte
Dictionary<int, Type1> sample1 = new();    // ここまでで 493146 増加
Dictionary<float, Type2> sample2 = new();  // ここまでで 824622 増加
Dictionary<double, Type3> sample3 = new(); // ここまでで 1452248 増加

数値や参照よりも一気にコード量が増えています

Enumでも増えないパターン

// 元の生成コードサイズ: 691328967 Byte
Dictionary<Enum1, int> sample1 = new(); // ここまでで 488867 増加
Dictionary<Enum2, int> sample2 = new(); // ここまでで 494028 増加
Dictionary<Enum3, int> sample3 = new(); // ここまでで 499188 増加

1つ目は400KBも増えてます
2つ目以降は10KBも増えてません

これはジェネリックコードの再利用が起きてるから
TKeyが違ってもTValueが違ったり、
どの型でも同じメソッドしか使ってなければ
コードの再利用が起きてコード量が爆発的に増えたりはしないらしいです

Structの場合

// 元の生成コードサイズ: 691328967 Byte
Dictionary<Box1, Box1> sample1 = new(); // ここまでで 702468 増加
Dictionary<Box2, Box2> sample2 = new(); // ここまでで 1219690 増加
Dictionary<Box3, Box3> sample3 = new(); // ここまでで 1663662 増加

// 元の生成コードサイズ: 691328967 Byte
Dictionary<Box1, int> sample1 = new();   // ここまでで 692121 増加
Dictionary<Box2, float> sample2 = new(); // ここまでで 1207416 増加
Dictionary<Box3, uint> sample3 = new();  // ここまでで 1659707 増加

// 元の生成コードサイズ: 691328967 Byte
Dictionary<int, Box1> sample1 = new();    // ここまでで 686589 増加
Dictionary<float, Box2> sample2 = new();  // ここまでで 1198307 増加
Dictionary<double, Box3> sample3 = new(); // ここまでで 1639201 増加

Enumよりも生成量が増えてます
これは数値比較がEnumよりも複雑で、Equals() と GetHashCode()の実装が必要なためです

Tupleの場合

// 元の生成コードサイズ: 691328967 Byte
Dictionary<(int, int), (int, int)> sample1 = new();         // ここまでで 522435 増加
Dictionary<(float, float), (float, float)> sample2 = new(); // ここまでで 1078199 増加
Dictionary<(uint, uint), (uint, uint)> sample3 = new();     // ここまでで 1680577 増加

// 元の生成コードサイズ: 691328967 Byte
Dictionary<(int, int), int> sample1 = new();       // ここまでで 515495 増加
Dictionary<(float, float), float> sample2 = new(); // ここまでで 1063444 増加
Dictionary<(uint, uint), uint> sample3 = new();    // ここまでで 1066594 増加

// 元の生成コードサイズ: 691328967 Byte
Dictionary<int, (int, int)> sample1 = new();       // ここまでで 504080 増加
Dictionary<float, (float, float)> sample2 = new(); // ここまでで 1048925 増加
Dictionary<uint, (uint, uint)> sample3 = new();    // ここまでで 1637353 増加

Structと同じくらい増えています
tupleはValueTuple という構造体なためです

調査結果まとめ

例外はあるものの、
DictionaryはTKeyとTValueのパターンが増えることに
容量がおおよそ400KB増えていきます

Enumをintに変換したり、
データ本体ではなくIDをDictionaryに入れたり、
Tupleを結合してStringにしてからDictionaryに入れたり、

できる限りTKeyとTValueのパターンを使わすことで生成コード量を抑制できます

対策

実際問題、TKeyとTValueのパターンを抑制するのは難しいです

いくつか代替手段を調べたので記載しておきます

NativeHashMap

Unityはよりアンマネージメントで高速なライブラリを提供してます
https://docs.unity3d.com/Packages/com.unity.collections@0.0/api/Unity.Collections.NativeHashMap-2.html

メリット
・高速、IL2CPPでもコードが増えない
デメリット
・定義時にサイズ指定しないといけない
・追加ができない(コストがかかる)

List<Pair<Key, T>>

Listはコード生成してもDictionaryほどのコード生成は走りません
数KB程度です
KeyとValueをPairのListで保持し、取得時にループして検索することで同じことができます

メリット
・軽量、IL2CPPでもコードがほぼ増えない
・読みやすい
デメリット
・要素が増えると探索コストが増える(O(n))
・Uniqueチェックをしてくれない

独自実装

Dictionaryは本当にリッチなので、必要最低限の連想配列ラッパークラスを作るのもよいです

KeyがUniqueである必要があるか、動的追加が必要か、Sort済であるべきか、
サイズ数と取得速度のバランス、メモリと

今後の課題

ジェネリックをテンプレートにする場合にコードが大量生成されるので
本当はDictionaryどうこうではなく他の個所でも起こります

次に注意なのはLinq部分かな?
今度時間があればそっちも調べます

おまけ(メモリ)

MemoryProfilerの「Executable & Mapped」を見ながらコード変更して計測してみるとわかります

普通に考えて、C++コード全部を静的メモリに展開する必要はないので半信半疑です
DictionaryをListに変換することでMemoryProfilerの該当項目は減ったのは確認しましたが
それが実機でも使用メモリ削減できてるかは不明です

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?