問題
- リストをソートするときに、外部の変数を参照してソートする必要がある
-
List.Sort
に比較関数としてクロージャを渡すと GC Alloc が発生するのをどうにかしたい(ソートの頻度がそこそこあるため)
はじめに、リストをソートするときに GC Alloc が発生していることを、Unity のプロファイラで確認します。
var list = new List<int> { 1, 2, 5, 6, 10, 11, 12 };
_button1.onClick.AddListener(() =>
{
Profiler.BeginSample("GCAllocCheck: sort(closure)");
int p = 6;
// list を p との距離が近い順にソートしたい
list.Sort((a, b) => Math.Abs(a - p).CompareTo(Math.Abs(b - p)));
Profiler.EndSample();
Observable.NextFrame(FrameCountType.EndOfFrame).Subscribe(_ => EditorApplication.isPaused = true);
});
実行すると 104 B のゴミが発生していることが確認できます。
対応
IComparer インターフェースを実装したインスタンスを予め作成しておいて、そのインスタンスを使い回すようにします。
class ListComparer : IComparer<int>
{
// p を元にしてソートしたい
public int P { get; set; }
public ListComparer()
{
P = 0;
}
public int Compare(int a, int b)
{
return Math.Abs(a - P).CompareTo(Math.Abs(b - P));
}
}
上のような ListComarer を作っておき、これを List.Sort に渡します。
var list = new List<int> { 1, 2, 5, 6, 10, 11, 12 };
var comparer = new ListComparer();
_button1.onClick.AddListener(() =>
{
Profiler.BeginSample("GCAllocCheck: sort(closure)");
int p = 6;
// list を p との距離が近い順にソートしたい
list.Sort((a, b) => Math.Abs(a - p).CompareTo(Math.Abs(b - p)));
Profiler.EndSample();
Profiler.BeginSample("GCAllocCheck: sort(comparer)");
comparer.P = p; // p をソートするタイミングで更新
list.Sort(comparer); // comparer を使い回す
Profiler.EndSample();
Observable.NextFrame(FrameCountType.EndOfFrame).Subscribe(_ => EditorApplication.isPaused = true);
});
実行結果です。
GC Alloc が 0 B と表示されており、ゴミが発生していないことが確認できます。
捕捉
ちなみに、list.Sort((a, b) => a - b)
のように比較関数が外部環境を参照しない場合、比較関数は内部でキャッシュされて使い回されます(参考: Unityでのボクシングの殺し方、或いはラムダ式における見えないnewの見極め方)。
参考
- Unityでのボクシングの殺し方、或いはラムダ式における見えないnewの見極め方
-
C#のGCゴミとUnity(5.5)のコンパイラアップデートによるListのforeach問題解決について
- Unity のプロファイラの使い方を参考にさせていただきました