Reconfiguring sizeof and related functions into a unified MemoryLayout struct
01 何が問題だったのか
Swift には、型の大きさや整列(アラインメント)、配列中の要素間隔を調べるための関数として、sizeof、sizeofValue、strideof、strideofValue、alignof、alignofValue の6つのフリー関数が用意されていました。これらはおもに、ポインタや生のメモリを扱う unsafe な処理の内部で使う専門的な API です。
// Swift 2 時点の書き方
sizeof(Int.self) // 8
strideof(Int.self) // 8
alignof(Int.self) // 8
let x: UInt8 = 5
sizeofValue(x) // 1
strideofValue(x) // 1
alignofValue(x) // 1
C から持ち込まれた名前が標準ライブラリに散らばっている
sizeof などの名前は C 由来の慣習的な表現で、Swift 本体や LLVM 側に sizeof と呼ばれる概念があるわけではありません。それにもかかわらず、6つのフリー関数として標準ライブラリのトップレベルに並んでおり、map や filter、Dictionary のような日常的に使う API と同列に見えてしまいます。実際にはこれらはメモリを直接触るときにしか使わない特殊な道具で、API の表面積を無駄に広げているうえ、初心者がうっかり触る入口にもなっていました。
値版(-Value 系)が誤解を招く
sizeofValue(x) のような「値を渡すバージョン」は、一見すると「その値(インスタンス)そのもののサイズ」を返してくれそうに見えます。しかし実際に返るのは、その値の静的な型のサイズであって、Array のように内部に動的なバッファを持つ型でも、バッファ分のメモリは含まれません。そもそもサイズはあくまで型の性質で、個々の値の性質ではない、という立場を取るのが自然です。
標準ライブラリや GitHub 上のコードを調べても、型ベースの呼び出し(sizeof(T.self))に比べて値ベースの呼び出し(sizeofValue(x))は圧倒的に少なく、存在意義が薄いという実態もありました。
関連機能がバラバラに置かれている
さらに、サイズ・ストライド・アラインメントはいずれも「ある型のメモリレイアウト」という同じテーマに属するのに、6つの独立したフリー関数に分かれているため、関連性が見えづらい構成になっていました。新しいレイアウト系の機能を足したいときにも、また別のフリー関数を追加するしかなく、拡張の方向性もきれいに定まりません。
02 どのように解決されるのか
型のメモリレイアウトに関する情報を、新しく導入するジェネリックな構造体 MemoryLayout<T> の static プロパティに集約します。従来の6つのフリー関数(sizeof / sizeofValue / strideof / strideofValue / alignof / alignofValue)は、値版もふくめてすべて削除されます。
MemoryLayout<T> に 3 つの static プロパティを置く
MemoryLayout<T> は、型 T のレイアウト情報を取り出すためだけに用意された、インスタンスを持たないジェネリック構造体です。サイズ・ストライド・アラインメントは、それぞれ size / stride / alignment という static プロパティとして提供されます。
public struct MemoryLayout<T> {
/// T の連続したメモリ上の占有サイズ(バイト)
public static var size: Int { get }
/// Array<T> の中で、ある要素の先頭から次の要素の先頭までのバイト数
/// UnsafePointer<T> をインクリメントしたときに進むバイト数と同じ
public static var stride: Int { get }
/// T のデフォルトのアラインメント(バイト)
public static var alignment: Int { get }
}
使う側は、型パラメータに対象の型を指定して、該当するプロパティを参照します。
MemoryLayout<Int>.size // 8
MemoryLayout<Int>.stride // 8
MemoryLayout<Int>.alignment // 8
MemoryLayout<UInt8>.size // 1
MemoryLayout<(Int, Bool)>.stride // 16
size は T が連続して占めるメモリのバイト数を返します。クラス型の場合はインスタンスの内容によらず参照自体のサイズになり、stored property の数で変わることはありません。stride は配列中で要素をひとつ進めるときのバイト数で、size と一致しないこともあります(アラインメントの都合で末尾にパディングが入るケースなど)。alignment はその型を置いてよいアドレスの整列条件です。
値版は削除
sizeofValue(x) のような値ベースの API は、新しい MemoryLayout からは提供されません。サイズは型の性質であって値の性質ではない、というのが今回の立場です。既存コードで sizeofValue(x) のように書いていた箇所は、対象の型を明示して型ベースの呼び出しに書き換えます。
let x: UInt8 = 5
// Before (Swift 2)
sizeofValue(x) // 1
// After (Swift 3)
MemoryLayout<UInt8>.size // 1
// もしくは、対象の式から型を組み立てて
MemoryLayout<type(of: x)>.size // (`type(of:)` と組み合わせる想定)
実用上は、sizeofValue が使われていた箇所はほぼ固定の型を持っていたケースが多く、型を書き下すだけで十分置き換えられます。
既存コードの移行
Swift 2 までの sizeof / strideof / alignof(およびその -Value 系)は、すべて MemoryLayout<T>.size / .stride / .alignment に置き換わります。置換はほぼ機械的で、マイグレーション用の Fix-it により自動的に書き換えが補助されます。
// Before
let s = sizeof(Int.self)
let a = alignof(Double.self)
let t = strideof((Int, Bool).self)
// After
let s = MemoryLayout<Int>.size
let a = MemoryLayout<Double>.alignment
let t = MemoryLayout<(Int, Bool)>.stride
MemoryLayout に集約されたことで、サイズ・ストライド・アラインメントが同じ名前空間のもとに並び、関連する概念であることが見た目からも分かるようになりました。また、将来レイアウト関連の情報を足したくなったときも、新しいフリー関数を増やすのではなく MemoryLayout の static メンバとして追加できます。これらの API はメモリを直接扱う unsafe な処理のための専門的な道具だ、という位置付けもはっきりします。