C# 基礎からマスターまで — 第9回
GC、値の形式、割り当てコストでパフォーマンスを理解する
本ブログ作成文は、講義を目指して製作されました。 また、作成された文章の情報も間違っている可能性がありますので、ご注意ください。
C#はGCがメモリを管理しますが、割り当てコストは消えません。 特にゲームのように一定のフレーム時間が重要なプログラムでは、いつオブジェクトが作成されるかを知る必要があります。
値型と参照型
public readonly struct Damage
{
public int Value { get; }
public Damage(int value) => Value = value;
}
Damage first = new(10);
Damage second = first;
値型は、変数に値自体が入って代入されるとコピーされます。 参照形式は、管理ヒープ内のオブジェクトを指す参照がコピーされます。
値型が常にスタックにあり、参照型が常に遅いと覚えてはいけません。 実際の保管場所とコストは、使用状況、サイズ、ランタイム最適化によって異なります。
boxingを避ける
int score = 100;
object boxed = score;
int restored = (int)boxed;
値型をオブジェクトや実装したインタフェースで扱うと、boxingが発生することがある。 ジェネリックコレクションを使用すると、値型をオブジェクトに置き換えることなく保存できます。
var scores = new List<int>();
scores.Add(100);
繰り返し割り当ての削減
private readonly List<Monster> visibleMonsters = new(128);
void CollectVisible()
{
visibleMonsters.Clear();
foreach (Monster monster in monsters)
{
if (monster.IsVisible)
{
visibleMonsters.Add(monster);
}
}
}
リストを毎回新しく作成せずに容量のあるバッファを再利用すると、割り当て回数を減らすことができます。 文字列連結、キャプチャラムダ、LINQ結果生成も繰り返しパスで割り当ての原因になる可能性があります。
まず測定する
GC世代と収集方法はランタイムが管理し、短く生きるオブジェクトが無条件問題ではない。 読みやすさを犠牲にしてすべてのコードを事前に最適化すると、メンテナンスコストだけが大きくなる可能性があります。
プロファイラでクォータとストップが実際のボトルネックであることを確認したら、プーリング、バッファの再利用、値の種類を適用する必要があります。 パフォーマンスの最適化は、推測に文法を変える作業ではなく、測定されたコストの原因を減らす作業です。