C#のStructは使い方を間違えるとパフォーマンスが大きく落ちるような罠がいくつかあるので、それらを簡単にまとめていきたいと思います。
structの防衛的コピー
readonlyで宣言されたstructのプロパティやメソッドを呼び出すと、readonlyであることを保証するためにコピーを生成してそれを操作する挙動をします。
struct Foo
{
int num = 7;
public int NumProperty => num;
public int GetNum()
{
return num;
}
}
//------------------------------
{
readonly Foo foo = new Foo();
int tmp = 0;
tmp = foo.num; //フィールドにアクセス コピーなし
tmp = foo.NumProperty; //プロパティー呼び出し コピー発生
tmp = foo.GetNum(); //関数呼び出し コピー発生
}
防衛的コピーの対策
c#7.2からstructにreadonlyをつけれるようになりました。
このstructはすべてのフィールドにreadonlyをつけなければいけない制約がありますが、コピーが発生しなくなります。
readonly struct Foo
{
readonly int num = 7; //コピーなし
public int NumProperty => num; // コピーなし
public int GetNum() // コピーなし
{
return num;
}
}
構造体の参照渡し
構造体を引数に渡すときはコピーされて渡されます(値渡し)。
サイズの大きい構造体の場合は効率が悪いので、このコピーを無くす方法として参照渡しがあります。
個人的な参照渡しをするかどうかの判断は以下の2つです。
- 高頻度でその関数が呼び出される
- 構造体のサイズが16byteよりも大きい
c#7.2以降は3つの参照渡しがあります。
- ref (入力・出力)
- out (出力)
- in (入力)
in修飾子は少し特殊で読み取り専用の参照渡しでreadonlyな形で渡されます。
つまりreadonlyで宣言していない構造体でも、inで参照渡しした関数内でメソッド・プロパティを呼び出してしまうと防衛的コピーが発生します。inで参照渡しをする場合はreadonlyで定義された構造体に限定したほうがいいと思います。
void FooMethod(in Foo foo) {
// ここでプロパティ・メソッドにアクセスするとコピー発生
var tmp = foo.GetNum(); // コピー発生
}
Foo foo = new Foo();
FooMethod(foo);
boxing回避
構造体を参照型であるobject型にキャストするために、ヒープにコピーするのがboxingです。
このboxingが頻発するとガーベジコレクションが走ってパフォーマンスに影響を与えるので可能なら回避するべきです。
意図的にobject型にキャストすることは殆ど無いですが、等価判定でEqualsが実装されていない場合にboxingが発生します。
Foo foo1 = new Foo();
Foo foo2 = new Foo();
var isEquals = foo1.Equals(foo2); // Equalsが実装されていないためobjectにキャストされて比較される
IEqualsインターフェイスを実装すれば構造体のEqualsメソッドが呼ばれます。
struct Foo : IEquatable<Foo> {
int num;
public override bool Equals(object obj) {
return obj is Foo foo && Equals(foo);
}
public bool Equals(Foo other) {
return num == other.num;
}
}
辞書のキーとして使用する構造体の場合はGetHashCodeメソッドもオーバーライドしておくほうがいいです。
struct Foo : IEquatable<Foo> {
//...省略
public override int GetHashCode() {
return HashCode.Combine(num);
}
}
IEquatableインターフェイスの実装と、GetHashCodeの実装はVisualStudioの場合は自動実装することができます。
実装したい構造体を選択して [ctrl + .] → EqualsおよびGetHashCodeを生成する...
c# 10.0から追加されたrecord structではIEquatableインターフェイスがコンパイラで生成されるのでこちらを使用するのも手です。
メモリ構造の確認
sharplabを使うとClassやStructの実際のメモリ上の構造を確認できます。
参考資料
https://ufcpp.net/study/csharp/cheatsheet/ap_ver7_2/
https://qiita.com/fksk36/items/dfa7e7e6ab40ecdd4ee8