自分で使うことはなさそう。
成果物
情報源
クラスより構造体のほうがメモリ消費が少ない?
小規模なデータ構造には、クラスよりむしろ構造体を使用するほうが、アプリケーションが実行するメモリ割り当ての数を大幅に減らすことができます。
そうなの?
たとえば、次のプログラムでは、100 個のポイントの配列を作成し初期化します。 Point をクラスとして実装すると、101 個の別々のオブジェクトがインスタンス化されます。配列に1 個、残りは 100 個の要素に 1 個ずつです。
日本語がわかりにくいが、1個の先頭アドレスポインタ変数と、100個のPointインスタンス変数、という意味だろう。
public static void Main() { Point[] points = new Point[100]; for (int i = 0; i < 100; i++) points[i] = new Point(i, i); }
ドキュメントには書いてないが、たぶん以下のようなクラスだろう。比較するなら書いてくれよ……。
class Point { public int x, y; }
これに代わる方法が、ポイントを構造体にすることです。
それで本当にメモリが少なくなるの?
struct Point { public int x, y; public Point(int x, int y) { this.x = x; this.y = y; } }
ここでは 1 つのオブジェクトのみがインスタンス化されます。すなわち、配列に 1 個です。そして、Point インスタンスがその配列内にインラインで格納されます。
ええと、つまり、「new
した構造体のライフタイムはfor
ブロック内のみである。よって、1ループあたり1インスタンス分のメモリしか消費せずに済む」という意味か?
型 | メモリ確保場所 | 確保場所における解放方法 |
---|---|---|
クラス | ヒープ | GC(ガーベジコレクション)がいつか自動で |
構造体 | スタック | ブロック終端直後に必ず |
そしてfor
ブロック文の1つ外側のMain関数ブロックでは、それぞれ以下のようにメモリ確保されていると。
型 | Main関数側のメモリ確保内容 |
---|---|
クラス | 1個の先頭アドレスポインタ変数と、100個のPointインスタンス変数 |
構造体 | 1個の先頭アドレスポインタ変数と、1個(for1回分)のPointインスタンス変数 |
要するに、スタック(値型)変数である構造体は、new
したブロック文の終端に達すると即解放される。対して、ヒープ(参照型)変数であるクラスは、new
したブロック文の終端になっても即解放されず、参照されなくなった後にGCが自動的に解放する。だからfor
ブロック内でnew
しているなら構造体のほうがMain関数内におけるメモリ消費を少なく抑えることができる。
クラスと構造体は、そのメモリを解放する仕組みとタイミングが違う。ブロック終端後に即解放される構造体のほうが、メモリ消費を少なくしやすい。ということか。
この話は構造体がどうというより、スタックとヒープのメモリ解放における話だと思うのだが。
ドキュメントの最初にあった「メモリ割り当ての数を大幅に減らすことができます」は、「構造体はブロック文内で囲んで寿命を短くすることができる」という意味なのね。別にメモリ消費が少なくなるとは書いていないし。わかりにくい表現だけども。
構造体のほうがメモリ消費が大きい場合もある
- 構造体全体をコピーすることは通常、オブジェクト参照をコピーするよりも非効率である
- 割り当てと値パラメーターの引き渡しは参照型よりも構造体のほうが手がかかる
- 構造体への参照を作成できない(
in
,out
,ref
を除く)
構造体へのポインタ
C言語を学習したことがあるなら、構造体を関数の引数に渡す時、ポインタにしたほうがメモリ消費が少ないことを理解できるはず。
構造体のフィールドすべてを含んだサイズのメモリ確保をするより、その構造体のアドレスを示すポインタ変数のメモリを確保するほうが少なくて済む。
たとえばint
型100個のフィールドをもつ構造体をメモリ確保するより、その構造体のポインタ変数1個をメモリ確保したほうが1/100で済む。int型とポインタ変数の消費メモリは同一のはず。32bitマシンならどちらも32bit(4Byte)だろう。
struct S { int i0; int i1; ... int i99; }
メモリ確保 | サイズ(Byte) |
---|---|
構造体のメモリを確保する | 400 |
構造体のポインタ変数メモリを確保する | 4 |
構造体のメモリをコピーして渡すものを「値渡し」、構造体の参照(アドレス)をコピーして渡すものを「参照渡し」という。以下はメソッドM
の引数に構造体S
を渡すときの記述である。
void M( S s) {} // 値渡し void M(ref S s) {} // 参照渡し(渡す前に`new`必要。代入可) void M(out S s) {} // 参照渡し(渡す前に`new`不要。代入可) void M(in S s) {} // 参照渡し(渡す前に`new`必要。代入不可)
メモリ計測してみた
構造体S
とクラスC
をnew
したときのメモリをそれぞれ計測してみた。開始から終了までの間のメモリ差分値である。
|S|C -|-|- 1|-7224|-9184 2|-15720|-5864 3|-27880|10536 4|-3160|-7296 5|2968|-22160 6|-17856|-9968 7|40816|13064 8|7880|10448 9|30776|25040 0|10312|7480
10回ほどやってみた差分である。見事にバラバラ。まったく何もわからない。負数になるのが謎。GCのせいだろう。計測したい箇所以外のメモリ解放がGCによって自動的に行われているため計測できないのだろう。つまり、メモリ計測は不可能という結論……。
メモリ管理をGCにまかせているのだから仕方ないのか?
計測ログ
計測ログ
$ ./run.sh ===== S ===== 方法|差分|終了|開始 ----|----|----|---- Env|0|0|0 GC |-7224|4281112|4288336 ===== C ===== 方法|差分|終了|開始 ----|----|----|---- Env|0|0|0 GC |-9184|4275664|4284848 $ ./run.sh ===== S ===== 方法|差分|終了|開始 ----|----|----|---- Env|0|0|0 GC |-15720|4285728|4301448 ===== C ===== 方法|差分|終了|開始 ----|----|----|---- Env|0|0|0 GC |-5864|4276832|4282696 $ ./run.sh ===== S ===== 方法|差分|終了|開始 ----|----|----|---- Env|0|0|0 GC |-27880|4266264|4294144 ===== C ===== 方法|差分|終了|開始 ----|----|----|---- Env|0|0|0 GC |10536|4307224|4296688 $ ./run.sh ===== S ===== 方法|差分|終了|開始 ----|----|----|---- Env|0|0|0 GC |-3160|4276632|4279792 ===== C ===== 方法|差分|終了|開始 ----|----|----|---- Env|0|0|0 GC |-7296|4277744|4285040 $ ./run.sh ===== S ===== 方法|差分|終了|開始 ----|----|----|---- Env|0|0|0 GC |2968|4290288|4287320 ===== C ===== 方法|差分|終了|開始 ----|----|----|---- Env|0|0|0 GC |-22160|4272528|4294688 $ ./run.sh ===== S ===== 方法|差分|終了|開始 ----|----|----|---- Env|0|0|0 GC |-17856|4292656|4310512 ===== C ===== 方法|差分|終了|開始 ----|----|----|---- Env|0|0|0 GC |-9968|4275664|4285632 $ ./run.sh ===== S ===== 方法|差分|終了|開始 ----|----|----|---- Env|0|0|0 GC |40816|4321496|4280680 ===== C ===== 方法|差分|終了|開始 ----|----|----|---- Env|0|0|0 GC |13064|4300312|4287248 $ ./run.sh ^[[A^[[B===== S ===== 方法|差分|終了|開始 ----|----|----|---- Env|0|0|0 GC |7880|4285672|4277792 ===== C ===== 方法|差分|終了|開始 ----|----|----|---- Env|0|0|0 GC |10448|4296096|4285648 $ ./run.sh ===== S ===== 方法|差分|終了|開始 ----|----|----|---- Env|0|0|0 GC |30776|4315288|4284512 ===== C ===== 方法|差分|終了|開始 ----|----|----|---- Env|0|0|0 GC |25040|4313920|4288880 $ ./run.sh ===== S ===== 方法|差分|終了|開始 ----|----|----|---- Env|0|0|0 GC |10312|4292728|4282416 ===== C ===== 方法|差分|終了|開始 ----|----|----|---- Env|0|0|0 GC |7480|4291448|4283968
コード
S.cs
class S { MemoryMeasurer mm = new MemoryMeasurer(); public void Run() { Console.WriteLine("===== S ====="); Point[] points = new Point[100]; mm.Start(); // この時点とメモリと for (int i = 0; i < 100; i++) { points[i] = new Point(i, i); } mm.Stop(); // この時点のメモリを取得する mm.Show(); // その差分を表示する } struct Point { int x, y; public Point(int x, int y) => (this.x, this.y) = (x, y); } }
C.cs
class C { MemoryMeasurer mm = new MemoryMeasurer(); public void Run() { Console.WriteLine("===== C ====="); Point[] points = new Point[100]; mm.Start(); for (int i = 0; i < 100; i++) { points[i] = new Point(i, i); } mm.Stop(); mm.Show(); } class Point { int x, y; public Point(int x, int y) => (this.x, this.y) = (x, y); } }
class MemoryMeasurer { long[] envs = new long[3]; long[] gcs = new long[3]; public void Start() { SetMemory(ref envs[0], ref gcs[0]); } public void Stop() { SetMemory(ref envs[1], ref gcs[1]); } public void Show() { CalcDiff(); Console.WriteLine("方法|差分|終了|開始"); Console.WriteLine("----|----|----|----"); Console.WriteLine($"Env|{envs[2]}|{envs[1]}|{envs[0]}"); Console.WriteLine($"GC |{gcs[2]}|{gcs[1]}|{gcs[0]}"); } private void SetMemory(ref long env, ref long gc) { env = Environment.WorkingSet; gc = GC.GetTotalMemory(true); } private void CalcDiff() { envs[2] = envs[1] - envs[0]; gcs[2] = gcs[1] - gcs[0]; } }
Program.cs
class Program { static void Main() { new S().Run(); new C().Run(); } }
run.sh
csc -nologo -recurse:*.cs -nullable:enable -langversion:preview chmod 755 ./Program.exe ./Program.exe
以下参考
- http://cammy.co.jp/technical/2017/05/23/c-メモリサイズ、メモリ使用量などを取得する/
- https://takachan.hatenablog.com/entry/2015/07/07/115839
対象環境
- Raspbierry pi 3 Model B+
- Raspbian stretch 9.0 2018-11-13 ※
- bash 4.4.12(1)-release ※
- SQLite 3.29.0 ※
- C# dotnet 3.0.100 ※
$ uname -a Linux raspberrypi 4.19.42-v7+ #1218 SMP Tue May 14 00:48:17 BST 2019 armv7l GNU/Linux