今回は、普段の開発では触れる機会が少ない MemoryLayout
とそれに関係した Data
について書きます。
MemoryLayout
以前
Swift3.0
以前は、同じ役割を sizeof
や strideof
等が担っていました。こちらの方が見覚えのある方は多いと思います。
MemoryLayout
は以下の Proposal で提案されました。
Reconfiguring sizeof
and related functions into a unified MemoryLayout
struct
Motivation には
関数を一つのネームスペースにまとめることで検索性を向上させる。
といったことが書かれています。(LLVM
の呼び出しにも触れられていますが、そこは調べられていません。)
MemoryLayout
の概要
MemoryLayout
は、それぞれの型に対するデータのメモリ上の配置に関する情報を取得することができます。情報は、size
, stride
, alignment
の3種類があります。それぞれ
情報 | 説明 |
---|---|
size |
一つの値が必要とするメモリ上のバイト数 |
stride |
配列中で一つの値が必要とするメモリ上のバイト数 |
alignment |
配列中の値のバイト数はこの値の整数倍である必要がある |
という内容です。alignment
は特に分かりにくいですね。stackoverflow の以下の投稿にある Ray Fix
さんの返信が理解の助けになってくれます。
stackoverflow - Swift: How to use sizeof?
サンプルコードの一部を引用します。
MemoryLayout<Foo>.size // returns 9
MemoryLayout<Foo>.stride // returns 16 because of alignment requirements
MemoryLayout<Foo>.alignment // returns 8, addresses must be multiples of 8
上から、
-
size
-
Foo
クラス単体ではメモリ上の9
バイトを占める。
-
-
alignment
- 要素が並んだ時、一つの要素が占めるメモリは
8
バイトの倍数でなければならない。
- 要素が並んだ時、一つの要素が占めるメモリは
-
stride
- 配列中で一つの要素が必要とするバイト数。これは
9
バイトではなくalignment
の整数倍でなければならないので、要素は16
バイトを必要とする。
- 配列中で一つの要素が必要とするバイト数。これは
という情報と読めます。
Data
と値の相互変換
MemoryLayout
を一部利用して、Data
と値の変換に関する振る舞いを見てみましょう。
Int
と Data
を相互に変換する
以下の stackoverflow の投稿が参考になりました。
stackoverflow - round trip Swift number types to/from Data
・ Int
→ Data
var num: Int = 6
let data = Data(buffer: UnsafeBufferPointer(start: &num, count: 1))
// data: <06000000 00000000>
// 上のコメントアウト部は `data` の内容を16進数で表示したものです。
// NSDataにキャストして `print` することで表示することができます
・ Data
→ Int
let num: Int = data.withUnsafeBytes { $0.pointee }
// 6
withUnsafeBytes
関数は型推論やジェネリクス、pointee
など要素が多く、理解が大変そうです。
・ [Int32]
-> Data
var nums: [Int32] = [2, 11, 17]
let data = Data(buffer: UnsafeBufferPointer(start: nums, count: nums.count))
// data: <02000000 0b000000 11000000>
// 32ビットごとに 2, 11, 17 が入っていることが分かります。
データが32ビットでまとまっていると見やすいので Int32
にしています。nums
の先頭から count(3)
個分の Int32
を変換です。
・ Data
-> [Int32]
let values: [Int32] = data.withUnsafeBytes {
[Int32](UnsafeBufferPointer(start: $0, count: data.count/MemoryLayout<Int32>.stride))
}
// [2, 11, 17]
count
のところは データ長/各要素のメモリ長
で要素数を計算しています。
簡単なデータは相互に変換できることが分かりました。String
は少し方法が違いますが簡単に出来ます。
Structの場合を見てみましょう。
Struct と Data
の相互変換を試みる
シンプルな要素を持つStruct
Int8
を要素としてもつ Struct Number
を定義します。
struct Number {
let value: Int8
}
Data
に変換して、続けて Data
から Numbers
にも変換します。
var value: Number = Number(value: 10)
let data = Data(buffer: UnsafeBufferPointer(start: &value, count: 1))
// value: 10
// <0a>
let restored_value: Number = data.withUnsafeBytes { $0.pointee }
// restored_value: 10
シンプル。データには 10
を表す <0a>
しか見えません。
複雑な構造の要素を持つStruct
Int
の配列を要素としてもつ Struct Numbers
を定義します。
struct Numbers {
let values: [Int]
}
Data
に変換して、続けて Data
から Numbers
にも変換します。
var value: Numbers = Numbers(values: [1, 2, 3])
let data = Data(buffer: UnsafeBufferPointer(start: &value, count: 1))
// value: [1, 2, 3]
// data: <40480700 00600000>
let restored_value: Numbers = data.withUnsafeBytes { $0.pointee }
// restored_value: [1, 2, 3]
無事できたようです。ですがデータの内容がさっきとだいぶ違いますね。
要素を変えながらデータを観察してみます。
var value: Numbers = Numbers(values: [1])
let data = Data(buffer: UnsafeBufferPointer(start: &value, count: 1))
// data: <a09d0500 00600000>
var value: Numbers = Numbers(values: [1, 2, 3, 4, 5])
let data = Data(buffer: UnsafeBufferPointer(start: &value, count: 1))
// data: <b0850900 80600000>
あれ、要素が増えても data
のバイナリデータは大きくならない。。
試しに、Int
の配列から Numbers
を生成、データへ変換して返却する関数を定義、それを使って同じ処理の結果を見てみます。
func numbersData(from values: [Int]) -> Data {
var value: Numbers = Numbers(values: values)
return Data(buffer: UnsafeBufferPointer(start: &value, count: 1))
}
let data: Data = numbersData(from: [1, 2, 3])
let restored_value: Numbers = data.withUnsafeBytes { $0.pointee } // ここで error: SIGABRT !!!
落ちました。
おそらく大きさが可変な要素を持つ Struct は、バイナリとして値自体を保持しているのではなく、ポインタを保持しているようです。関数の中でデータの生成に使った配列データは、関数の外ではスコープ外になり、ポインタが指すアドレスの先にはアクセスできるデータはなくなってしまっているように見えます。
つまり、ここで得られたデータは有効なスコープ内でしか生きられない、__永続化の為には使えない__ということです。
そして、永続化の為に
ライブラリを作りました。
https://github.com/naru-jpn/pencil
標準的な Int
, String
, [Int]
, ... 等はもちろん、作成した Struct も簡単に永続化できます。(ちょっと Data
の変換処理とかイケてない箇所が結構残ってますが)お時間のある時に覗いてみてください。
お粗末様でした。