Swiftのメモリ割り当てを知る
本記事では、Swiftにおけるメモリ割り当てについて解説します。
メモリ
メモリとは、プロセスを実行するために必要なデータを保持しておく領域です。定数や変数、それからプログラムの命令列など、様々な値を保持します。プロセスが実行されると、CPUはそのプロセスが持っているメモリデータに対して演算を行います。
メモリ配置
メモリは巨大な配列のようなもので、そのまま使おうとすると少し扱いづらくなります。したがってプロセスは、割り当てられたメモリをいくつかのカテゴリーに分けて利用します。これをメモリ配置(Memory Layout)といいます。
Instructionsにはコード、つまりプログラムの命令列が入り、Global dataにはstaticのようなグローバルデータが入ります。残りのスペースをHeapとStackに割り当てますが、これらは実際の大きさがコンパイル時には定まりません。なので、残りのメモリの両端にHeap及びStackの先頭を割り当て、そこから各々利用していきます。
Stack
StackにはFunction callと値型のデータが割り当てられます。前回の記事において、値型はスコープを抜け次第すぐに解放されると記述しましたが、これはStackによって管理されているためです。
Stackにはまず関数が積まれ、その上にスコープ内で利用するデータたちが積まれます。スコープを抜けるとそれらのデータはStackから取り除かれます。関数を抜けた時も同じくその関数がStackから取り除かれます。
Break pointでプログラムを止めた時、Xcodeの左ペインに実行中の関数が表示されますが、これがまさにStackです。下から順番に、今実行されている関数が積まれています。
Heap
Heapには参照型のデータが割り当てられます。インスタンスを生成した時にはまずHeapに領域が割り当てられ、その領域を示すポインタがStackに積まれます。
Heapは上述のような処理が必要であること、Stackよりも構造が複雑であること、スレッドセーフに扱わなければいけないことなどから、Stackよりも処理に時間がかかります。
Heapに割り当てられる値型
値型のデータは通常Stackに割り当てられますが、場合によってはHeapに割り当てられることもあります。以下のような状況の時、値型であってもそのデータはHeapに割り当てられます。
- 値型のサイズがコンパイル時に確定しない場合
- 値型が参照型を保持している場合
- 値型が参照型によって保持されている場合
Heapに割り当てられる値型は、様々な点でパフォーマンスを低下させます。まず挙げられるのがコピーされた時のメモリ割り当てです。値型はcopy-on-assignmentといって、コピーされた時点でメモリが確保される方式でコピーされます。
一方で参照型は、copy-on-writeという、書き換えられた時にメモリが確保される方式でコピーされます。参照型はメモリ割り当て処理に時間がかかりますが、割り当てが行われるのは実際に書き換えられた時になので、コピーする分には問題にはなりません。
しかしながらHeapに割り当てられる値型は、コピーされた時にHeapにメモリが割り当てられます。したがって、大量にコピーすると処理に非常に時間がかかってしまいます。
また、前回の記事で解説した参照カウントも処理速度を低下させる要因の1つです。参照型を保持する参照型がコピーされた場合は、親の参照カウントを増やすだけで済みます。一方で参照型を保持する値型がコピーされた場合には、プロパティである参照型のデータ1つひとつの参照カウントを増やさなければなりません。
以上のような理由から、Heapに割り当てられる値型は参照型よりもパフォーマンスを低下させます。したがってこのような場合には、値型ではなく参照型として宣言した方が良いです。