導入
Swiftには値型と参照型があります。また、Optionalはそれらと組み合わせてよく使われます。メモリ上ではどのように表現されているのでしょうか。調べてみました。(Swift 3.0.2)
メモリダンプ関数
メモリの中身を見るために下記のような関数を作りました。
func dumpMemory(_ p: UnsafeRawPointer, _ size: Int) {
let addr = UInt64(UInt(bitPattern: p))
let p = p.bindMemory(to: UInt8.self, capacity: size)
for i in 0..<size {
if i % 8 == 0 {
if i != 0 {
print()
}
print(String(format: "[0x%016lx]", arguments: [addr + UInt64(i)]),
separator: "", terminator: "")
}
print(String(format: " %02x", arguments: [p[i]]),
separator: "", terminator: "")
}
print()
}
テストする型
メモリ内でわかりやすいように、下記のようなstructとclassを作りました。それぞれ4バイト変数を4つもつので16バイトのデータになります。
struct SA {
var v1: UInt32 = 0xA1B1C1D1
var v2: UInt32 = 0xA2B2C2D2
var v3: UInt32 = 0xA3B3C3D3
var v4: UInt32 = 0xA4B4C4D4
}
class CA {
var v1: UInt32 = 0xA1B1C1D1
var v2: UInt32 = 0xA2B2C2D2
var v3: UInt32 = 0xA3B3C3D3
var v4: UInt32 = 0xA4B4C4D4
}
構造体のメモリレイアウトを見る
UnsafeMutablePointer<T>
のコンストラクタに &
を付けてTの型の変数を渡すと、その変数へのポインタを作ることができます。これは関数の引数が UnsafeMutablePointer<T>
型であるときの特別なルールで、 inout T
の式を自動で変換してくれます。
An in-out expression of type Type that contains a mutable variable, property, or subscript reference, which is passed as a pointer to the address of the mutable value.
こちらのドキュメントの Mutable Pointers の章に書いてあります。Interacting with C APIs
また、 T
のメモリサイズは MemoryLayout<T>.size
で調べることができます。これらを組み合わせて、スタック上の SA
型の変数を先述した dumpMemory
に渡してみます。
var x = SA()
let p = UnsafeRawPointer(UnsafeMutablePointer<SA>(&x))
dumpMemory(p, MemoryLayout<SA>.size)
出力
[0x00007fff588333e8] d1 c1 b1 a1 d2 c2 b2 a2
[0x00007fff588333f0] d3 c3 b3 a3 d4 c4 b4 a4
サイズはちょうど16バイトで、フィールドの v1
, v2
, v3
, v4
が順番に並んでいる事が確認できます。 0xA1B1C1D1
という値が d1 c1 b1 a1
と逆順の出力になっているのは、このIntel CPUがリトルエンディアンだからですね。
このようにSwiftの値型は、C, C++の値型と同様、余計なデータが付け足されていない 生の値型 であることがわかります。
クラスのメモリレイアウトを見る
では同じようにしてクラスのメモリレイアウトを見てみます。
var x = CA()
let p = UnsafeRawPointer(UnsafeMutablePointer<CA>(&x))
dumpMemory(p, MemoryLayout<CA>.size)
出力
[0x00007fff588333f0] 50 51 0f 4e 9d 7f 00 00
CA
は同じように16バイトのデータなのに、 x
のサイズは8バイトとなっています。これはポインタだからです。 UnsafeRawPointer
の bindMemory
メソッドを使って、 UnsafeRawPointer
(ポインタ) を UnsafePointer<UnsafeRawPointer>
(ポインタのポインタ) に変換してやれば、ポインタの参照先のメモリを dumpMemory
で見ることができます。問題は何バイト読めばよいかですが、ここはエスパーで 32
バイト読むことにします。
let p2 = p.bindMemory(to: UnsafeRawPointer.self, capacity: 1).pointee
dumpMemory(p2, 32)
出力
[0x00007f9d4e0f5150] c8 51 36 0b 01 00 00 00
[0x00007f9d4e0f5158] 04 00 00 00 02 00 00 00
[0x00007f9d4e0f5160] d1 c1 b1 a1 d2 c2 b2 a2
[0x00007f9d4e0f5168] d3 c3 b3 a3 d4 c4 b4 a4
お、16バイト後ろのところに CA
のデータがありました。前16バイトは何でしょうか。それは、swiftのソースコードを見に行くとわかります。swift/stdlib/public/SwiftShims/HeapObject.h これによると、先頭8バイトは CA
のクラス定義へのポインタです。次の4バイトは強参照カウンタです。そして最後の4バイトが弱参照カウンタです。
強参照を1つ増やしてみましょう。
var x2 = x
dumpMemory(p2, 32)
出力
[0x00007f9d4e0f5150] c8 51 36 0b 01 00 00 00
[0x00007f9d4e0f5158] 08 00 00 00 02 00 00 00
[0x00007f9d4e0f5160] d1 c1 b1 a1 d2 c2 b2 a2
[0x00007f9d4e0f5168] d3 c3 b3 a3 d4 c4 b4 a4
8バイト目の数値が4から8になりました。
弱参照を1つ増やしてみましょう。
weak var x3 = x
dumpMemory(p2, 32)
出力
[0x00007f9d4e0f5150] c8 51 4a 10 01 00 00 00
[0x00007f9d4e0f5158] 08 00 00 00 04 00 00 00
[0x00007f9d4e0f5160] d1 c1 b1 a1 d2 c2 b2 a2
[0x00007f9d4e0f5168] d3 c3 b3 a3 d4 c4 b4 a4
12バイト目の数値が2から4になりました。これらのカウンタはなぜこのような数値を取るのでしょうか。
それは、強参照は2ビット、弱参照は1ビットシフトした表現になっているからです。つまり、どちらも最初は1であり、それぞれ増やした事で2に変化したのです。これもswiftのソースコードでわかります。swift/stdlib/public/SwiftShims/RefCount.h
オプショナルな構造体のメモリを見る
Optional<SA>
のメモリを見てみます。
var x1: SA? = .some(SA())
let px1 = UnsafeRawPointer(UnsafeMutablePointer<SA?>(&x1))
dumpMemory(px1, MemoryLayout<SA?>.size)
出力
[0x00007fff536ee3e0] d1 c1 b1 a1 d2 c2 b2 a2
[0x00007fff536ee3e8] d3 c3 b3 a3 d4 c4 b4 a4
[0x00007fff536ee3f0] 00
サイズが1バイト増えて17バイトになりました。データは先頭0バイト〜16バイトに入っています。末尾に追加された1バイトがオプショナルの値の有無を表していそうです。noneの場合を見てみましょう。
var x2: SA? = .none
let px2 = UnsafeRawPointer(UnsafeMutablePointer<SA?>(&x2))
dumpMemory(px2, MemoryLayout<SA?>.size)
出力
[0x00007fff536ee3c8] 00 00 00 00 00 00 00 00
[0x00007fff536ee3d0] 00 00 00 00 00 00 00 00
[0x00007fff536ee3d8] 01
データ部分はゼロクリアされました。末尾の1バイトが1になっています。0がsome、1がnoneを表すようです。
このレイアウトは面白いですね。僕はフラグは先頭にあると予想していましたが末尾にありました。たしかに先頭に1バイト挟んでしまうと、データ部分のアライメントのためにフラグの1バイトの後にパディングを入れる必要が生まれて、17バイトより大きくなってしまう気がします。配列にしたりする場合は結局このフラグの後ろにパディングが入るのでサイズが大きくなってしまいますが、スタック変数の場合で、後ろに詰む変数の型が Int8
などであれば、無駄なく敷き詰めることができそうです。
多重オプショナルな構造体のメモリを見る
では、 Optional
が入れ子になったらどうなるのでしょうか。
var x1: SA?? = .some(.some(SA()))
let px1 = UnsafeRawPointer(UnsafeMutablePointer<SA??>(&x1))
dumpMemory(px1, MemoryLayout<SA??>.size)
var x2: SA?? = .some(.none)
let px2 = UnsafeRawPointer(UnsafeMutablePointer<SA??>(&x2))
dumpMemory(px2, MemoryLayout<SA??>.size)
var x3: SA?? = .none
let px3 = UnsafeRawPointer(UnsafeMutablePointer<SA??>(&x3))
dumpMemory(px3, MemoryLayout<SA??>.size)
出力
[0x00007fff536ee3e0] d1 c1 b1 a1 d2 c2 b2 a2
[0x00007fff536ee3e8] d3 c3 b3 a3 d4 c4 b4 a4
[0x00007fff536ee3f0] 00 00
[0x00007fff536ee3c8] 00 00 00 00 00 00 00 00
[0x00007fff536ee3d0] 00 00 00 00 00 00 00 00
[0x00007fff536ee3d8] 01 00
[0x00007fff536ee3b0] 00 00 00 00 00 00 00 00
[0x00007fff536ee3b8] 00 00 00 00 00 00 00 00
[0x00007fff536ee3c0] 00 01
2重Optionalにしたところ、サイズがもう1バイト増えました。 .some(.none)
の場合は 01 00
という並びになっています。0がsomeですから、外側のオプショナルのフラグが後ろにあるようです。Optionalを入れ子にするたびにこれが伸びていくようですね。
オプショナルなクラスのメモリを見る
オプショナルなクラスのメモリを見てみましょう。
var x1: CA? = .some(CA())
let px1 = UnsafeRawPointer(UnsafeMutablePointer<CA?>(&x1))
dumpMemory(px1, MemoryLayout<CA?>.size)
出力
[0x00007fff536ee3f0] 60 51 72 28 ef 7f 00 00
普通に8バイトのポインタです。参照先を見てみます。
let ppx1 = px1.bindMemory(to: UnsafeRawPointer.self, capacity: 1).pointee
dumpMemory(ppx1, 32)
出力
[0x00007fef28725160] c8 51 4a 10 01 00 00 00
[0x00007fef28725168] 04 00 00 00 02 00 00 00
[0x00007fef28725170] d1 c1 b1 a1 d2 c2 b2 a2
[0x00007fef28725178] d3 c3 b3 a3 d4 c4 b4 a4
CA
のときと同じですね。では、noneの時はどうなるのでしょうか。
var x2: CA? = .none
let px2 = UnsafeRawPointer(UnsafeMutablePointer<CA?>(&x2))
dumpMemory(px2, MemoryLayout<CA?>.size)
出力
[0x00007fff536ee3e8] 00 00 00 00 00 00 00 00
結果はこのようにポインタが0になっていました。nullポインタですね。
これがswiftの良いところです。nullポインタを Optionalの none として扱っているため、Optionalを導入する上でのメモリコストを無しにできています。
多重オプショナルなクラスのメモリを見る
ではクラスのオプショナルを多重にしたらどうなるのでしょうか。値型のときのアプローチのように、ポインタの後ろに1バイト追加して9バイトにするのでしょうか。それとも、ヒープ上の値の後ろに1バイト追加して17バイトにするのでしょうか。
var x1: CA?? = .some(.some(CA()))
let px1 = UnsafeRawPointer(UnsafeMutablePointer<CA??>(&x1))
dumpMemory(px1, MemoryLayout<CA??>.size)
出力
[0x00007fff536ee3f0] 60 51 72 28 ef 7f 00 00
CA??
それ自体は8バイトのようです。普通のポインタですね。参照先を見てみます。
let ppx1 = px1.bindMemory(to: UnsafeRawPointer.self, capacity: 1).pointee
dumpMemory(ppx1, 32)
出力
[0x00007fef28725160] c8 51 4a 10 01 00 00 00
[0x00007fef28725168] 04 00 00 00 02 00 00 00
[0x00007fef28725170] d1 c1 b1 a1 d2 c2 b2 a2
[0x00007fef28725178] d3 c3 b3 a3 d4 c4 b4 a4
32バイトでこれまでと同じレイアウトです。ちなみに、ここを33バイト目までダンプすると、実行するたびに33バイト目の値が変化するので関係無いことがわかります。さて、このままだと入れ子分の情報を入れる場所がありません。 .some(.none)
を見てみましょう。
var x2: CA?? = .some(.none)
let px2 = UnsafeRawPointer(UnsafeMutablePointer<CA??>(&x2))
dumpMemory(px2, MemoryLayout<CA??>.size)
出力
[0x00007fff536ee3e8] 00 00 00 00 00 00 00 00
普通にnullポインタになりました。では、 noneを見てみましょう。
var x3: CA?? = .none
let px3 = UnsafeRawPointer(UnsafeMutablePointer<CA??>(&x3))
dumpMemory(px3, MemoryLayout<CA??>.size)
出力
[0x00007fff536ee3e0] 02 00 00 00 00 00 00 00
なんという事でしょう・・・。 値が2のポインタ になっています。たしかにこれならメモリサイズのゼロコストを保つことができます。しかし、 アドレス=2 のポインタが存在できなくなる事も意味します。
入れ子が増えたらどうなるのでしょうか。一気に5重オプショナルで調べてみます。
var x1: CA????? = .some(.some(.some(.some(.some(CA())))))
let px1 = UnsafeRawPointer(UnsafeMutablePointer<CA?????>(&x1))
dumpMemory(px1, MemoryLayout<CA?????>.size)
let ppx1 = px1.bindMemory(to: UnsafeRawPointer.self, capacity: 1).pointee
dumpMemory(ppx1, 32)
var x2: CA????? = .some(.some(.some(.some(.none))))
let px2 = UnsafeRawPointer(UnsafeMutablePointer<CA?????>(&x2))
dumpMemory(px2, MemoryLayout<CA?????>.size)
var x3: CA????? = .some(.some(.some(.none)))
let px3 = UnsafeRawPointer(UnsafeMutablePointer<CA?????>(&x3))
dumpMemory(px3, MemoryLayout<CA?????>.size)
var x4: CA????? = .some(.some(.none))
let px4 = UnsafeRawPointer(UnsafeMutablePointer<CA?????>(&x4))
dumpMemory(px4, MemoryLayout<CA?????>.size)
var x5: CA????? = .some(.none)
let px5 = UnsafeRawPointer(UnsafeMutablePointer<CA?????>(&x5))
dumpMemory(px5, MemoryLayout<CA?????>.size)
var x6: CA????? = .none
let px6 = UnsafeRawPointer(UnsafeMutablePointer<CA?????>(&x6))
dumpMemory(px6, MemoryLayout<CA?????>.size)
出力
[0x00007fff4fec63f0] 30 ce f1 81 fc 7f 00 00
[0x00007ffc81f1ce30] c8 51 4a 10 01 00 00 00
[0x00007ffc81f1ce38] 04 00 00 00 02 00 00 00
[0x00007ffc81f1ce40] d1 c1 b1 a1 d2 c2 b2 a2
[0x00007ffc81f1ce48] d3 c3 b3 a3 d4 c4 b4 a4
[0x00007fff4fec63e8] 00 00 00 00 00 00 00 00
[0x00007fff4fec63e0] 02 00 00 00 00 00 00 00
[0x00007fff4fec63d8] 04 00 00 00 00 00 00 00
[0x00007fff4fec63d0] 06 00 00 00 00 00 00 00
[0x00007fff4fec63c8] 08 00 00 00 00 00 00 00
なんと、ポインタの値が2ずつ増加していきました。一番根本が none になる場合が 0ポインタで、noneが1つ手前に上がるごとにポインタが2ずつ増えていきます。
終わり
Swiftはポインタに値を仕込んでいる事がわかりました。しかしこれだと、値を仕込むためにアドレスを一部使ってしまうので、ポインタの表現範囲に制約が生じます。実際出てくるようなアドレスはもっとでかい値なので問題にはならないのだと思いますが、設計としてどこまでの数値をメタ情報に使う事ができるようになっているのか気になります。残念ながら今回はその部分まではswiftソースから発見できなかったので、今後の課題とします。