プロパティの宣言順番によってメモリサイズが異る
以下のFooとBarは、どちらもInt型とInt8型,Int32型のプロパティだけを持つ値型ですが、
それぞれ確保されるメモリサイズが異なります。
※ 1.3 GHz Intel Core i5 (64bit)の場合
struct Foo {
let a: Int
let b: Int8
let c: Int32
}
print(MemoryLayout<Foo>.size) // 16
struct Bar {
let b: Int8
let a: Int
let c: Int32
}
print(MemoryLayout<Bar>.size) // 20
どうしてこのような結果になるのか、
MemoryLayoutで定義されている以下の3つの型プロパティをもとに考えたいと思います。
各プロパティについて
以下リファレンスを自分なりに意訳してみました。
name | 説明 |
---|---|
alignment | 型Tのデフォルトのメモリアライメント値。単位はバイト。 unsafe pointerを使ってメモリーを確保する際は、このalimentプロパティを使用すること。 この値は常に正。 |
size | 型Tが必要とするメモリ容量(バイト数)。 この値には、動的に生成されたメモリや離れた箇所へのメモリ領域は含まれない。 とりわけ、型Tがclassの場合は、定義されているストアドプロパティの数は関係しない。 またunsafe pointerを使用して、複数のTインスタンスのメモリーを確保する際には、sizeの代わりにstrideプロパティを使用すること。 |
stride | Array<T>もしくは連続して型Tインスタンスを確保した際に、型Tインスタンスの開始から次のインスタンスまでのバイト数。 この値は、UnsafePointer<T>インスタンスがインクリメントされる時に移動されるバイト数と同一である。 型Tは効率的なメモリ空間と引き換えに、最適なランタイムパフォーマンスを提供する最小のアライメント値を持つ。 |
疑問
リファレンスの説明は難しい。。。以下僕に生じた疑問です。
- alignmentの説明が分からない。そもそもalignmentとは何なのか?
- なぜ連続して型Tインスタンスのメモリーを確保する時は、sizeではなくstrideなのか?
まずalignementについて調べようと思います。
実はこれが前述したFooとBarのサイズが異る理由になっていました。
疑問1 alignmentとは
alignmentの必要性を理解するためには、
CPUがメインメモリからどのようにデータを読み込んでいるかを理解する必要があります。
alignmentは、この読み込みを最適化し、かつメモリリソースの効率化を実現するための仕組みだからです。
CPUがメインメモリーからデータを読み取る仕組みについて
CPUがメインメモリーからデータを読み取るまでのフロー
- CPUはメインメモリーに対してアドレスバスを通じて読み取りたいデータのアドレスを伝える。
- メインメモリーは1の要求を受けてデータバスに該当のメモリアドレスに格納されているデータを出力する。
- CPUはデータバスを通して、データを受け取る。
つまりCPUとメインメモリーに関して以下のことがいえます。
CPUとメインメモリーについて
-
メモリアドレスは64bitの場合、8バイトずつ割り振られている。(32bitは4バイト、16bitは2バイトずつ。またこの値は アドレス長 と表現される。)
-
CPUはメモリーに対してアドレス長毎にしかデータを取得(or格納)できない。
もし下図のように、4バイトのデータaと8バイトのデータbを詰めてメモリーに格納してしまった場合、
データbの値を取得するのにCPUは、メインメモリーから2回データを読み取る必要が発生する。
-
アドレス長(ここでは8バイト)毎にデータを格納すれば、一度に大きな値を取得(or格納)することが出来る。
-
闇雲にアドレス長毎にデータを格納していくと、 小さなデータは無駄にメモリをがめる事になる。
また上図の場合、データa,b,cを同一のアドレス位置に格納しても、データbの取得は一回で行え、また省メモリにもつながる。
そこで各型において、 CPUからの読み取り回数を減らし、かつメモリーもなるべく収まるような最適な位置にデータが割り当てるための値 を考えます。
これがalignment値です。
alignmentとは
つまりalignmentは、CPUの最適なパフォーマンスとなるべくデータの省メモリー化を実現するための兼ね合いから考え出された妥協の産物だといえます。
ここでSwift Standard Libraryで用意されている基本型のaliment値を確認すると以下の用になっていました。
※ 1.3 GHz Intel Core i5 (64bit)の場合
型 | alignment | size | stride |
---|---|---|---|
Int8 | 1 | 1 | 1 |
Int16 | 2 | 2 | 2 |
Int32 | 4 | 4 | 4 |
Int64 | 8 | 8 | 8 |
Int | 8 | 8 | 8 |
つまりInt8はalignmentの値が1なので、メモリーアドレス値が 1の倍数 の位置に格納されるということになります。
またInt16は 2の倍数, Int32は 4の倍数, Int64は 8の倍数 の位置に。
これはsizeの値と一緒に考えると、
例えば型Intの場合、sizeは8バイトなので、alignment値が8であれば、メインメモリーに対して8の倍数となる位置に読み込まれるため、一つのメモリアドレス空間にデータを格納できます。
※ SwiftのInt型のsizeはアドレス長です。
このaliment値から、FooとBarのメモリ構成は以下のようになっていると考えれれます。
FooとBarのメモリ構成
Fooのメモリ構成
struct Foo {
let a: Int
let b: Int8
let c: Int32
}
print(MemoryLayout<Foo>.size) // 16
占めているメモリ容量 = a(8) + b(1) + 灰色(3) + c(4) = 16
Barのメモリ構成
struct Bar {
let b: Int8
let a: Int
let c: Int32
}
print(MemoryLayout<Bar>.size) // 20
占めているメモリ容量 = b(1) + 灰色(7) + a(8) + c(4) = 20
それぞれMemoryLayout<T>.sizeで取得した値になっています。
疑問2 Strideの役割
sizeプロパティの説明にて、
型Tインスタンスを連続して生成する際は、sizeではなく、stride値を使用すること
とあり、またstrideの定義は以下のように説明されています。
Array<T>もしくは連続して型Tインスタンスを確保した際に、型Tインスタンスの開始から次のインスタンスまでのバイト数。
この値は、UnsafePointer<T>インスタンスがインクリメントされる時に移動されるバイト数と同一である。
このStride値について、再度型Barを用いて考えたいと思います。
MemoryLayout<Bar> 各プロパティ値
struct Bar {
let b: Int8
let a: Int
let c: Int32
}
MemoryLayout<Bar>における各値
alignment | size | stride |
---|---|---|
8 | 20 | 24 |
つまり型Barのalignment値は8なので、型Barインスタンスは、アドレス値が8の倍数の位置にしか格納出来ないことが分かる。
従って、size値を用いて連続的にBarインスタンスを確保することが出来ない。
そこでalignmentが考慮されたstride値が必要となると考えられる。(下図)
最後に
FooとBarのメモリ構成は最適化を指定して実行した際も異なっていました。
つまり型を定義する際に、alignment値を考慮すれば、ある程度は省メモリ化することが出来るということがいえます。
参考サイト
アライメントについて
-
データ型のアラインメントとは何か,なぜ必要なのか?
今回のアライメントの理解はこのサイト無くしてはありえませんでした。感謝です。
MemoryLayout<T>について
-
Swiftのメモリレイアウトを調べる
データをダンプしてその実態まで調査されている力に脱帽です。
今回、自分がバイナリ値を直接読み取る力がないため、図にしてみようと思ったきっかけになった記事です。 -
MemoryLayout and Data in Swift3
MemoryLayout<T>の使い方に非常に参考にさせていただきました。