Accessing an Array’s Uninitialized Buffer
01 何が問題だったのか
Swift の Array には、容量を確保したうえで「未初期化のままバッファに直接書き込む」ための公式な手段がありませんでした。そのため、一度にまとめて要素を書き込みたいだけの場面でも、不要なデフォルト初期化や余計なコピーが避けられませんでした。
未初期化バッファを扱えない
たとえば、元の配列を「条件を満たす要素を前から、それ以外を後ろから」詰めていく stable partition は O(n) で実装できるはずのアルゴリズムです。しかし Swift では、次のような理由からこれを効率よく書く手段がありませんでした。
- 特定のサイズの配列を、全要素を初期化せずに作る API がない。
Arrayのバッファの末尾側にだけ要素を書き込み、あとから先頭側を埋める、といった順序での初期化ができない。UnsafeMutableBufferPointerを自前で確保すれば任意の順序で初期化できますが、そこからArrayに「コピーせずに」移し替える手段がない。Arrayの記憶領域はバッファ先頭に count や capacity を含むため、外から作った生バッファをそのままArrayの実体として引き渡すこともできません。
結果として、可能な限り効率的な実装を書こうとしても、一度ダミー値でバッファ全体を初期化する、あるいは UnsafeMutableBufferPointer から Array にコピーする といった、避けたいオーバーヘッドが残ってしまっていました。
C API との相互運用でも同じ問題
要素数が不明のまま T* バッファを埋めて、書き込んだ個数を返すような C API を呼び出すケースでも状況は同じです。あらかじめ Array をダミー値で埋めてから渡すか、UnsafeMutableBufferPointer を使って呼び出したあとに Array に詰め直すかのいずれかが必要で、どちらも本質的には不要な作業です。
このように、「容量だけ決まった Array のバッファに、任意の順序で安全に書き込みたい」という用途は現実的によくあるにもかかわらず、標準ライブラリでは素直に書けない状況でした。
02 どのように解決されるのか
Array と ContiguousArray に、未初期化バッファに直接アクセスできるイニシャライザと、既存配列の記憶領域を未初期化部分まで含めて扱えるメソッドの2つを追加することを提案していました。
init(unsafeUninitializedCapacity:initializingWith:)
容量を指定して Array を作り、クロージャの中で未初期化バッファに直接書き込めるイニシャライザです。クロージャは UnsafeMutableBufferPointer と、初期化済み要素数を表す inout Int を受け取り、クロージャ終了時点で buffer[0..<initializedCount] が初期化済み、残りが未初期化 という状態を満たす必要があります。
var myArray = Array<Int>(unsafeUninitializedCapacity: 10) { buffer, initializedCount in
for x in 1..<5 {
buffer[x] = x
}
buffer[0] = 10
initializedCount = 5
}
// myArray == [10, 1, 2, 3, 4]
Array の実際の capacity は要求より大きくなる場合もありますが、クロージャに渡されるバッファは 要求した要素数ちょうどの長さ になります。これは「要求容量よりバッファが大きい」ことを前提にした誤ったコードを書きにくくするためです。
このイニシャライザを使うと、stable partition を余計なコピーなしで書けます。バッファの先頭から「条件を満たす要素」を、末尾から「そうでない要素」を書き込んでいき、最後に末尾側の区間を反転させるだけで済みます。
func stablyPartitioned(by belongsInFirstPartition: (Element) throws -> Bool) rethrows -> [Element] {
return try Array<Element>(unsafeUninitializedCapacity: count) {
buffer, initializedCount in
var low = buffer.baseAddress!
var high = low + buffer.count
do {
for element in self {
if try belongsInFirstPartition(element) {
low.initialize(to: element)
low += 1
} else {
high -= 1
high.initialize(to: element)
}
}
let highIndex = high - buffer.baseAddress!
buffer[highIndex...].reverse()
initializedCount = buffer.count
} catch {
let lowCount = low - buffer.baseAddress!
let highCount = (buffer.baseAddress! + buffer.count) - high
buffer.baseAddress!.deinitialize(count: lowCount)
high.deinitialize(count: highCount)
throw error
}
}
}
withUnsafeMutableBufferPointerToStorage(capacity:_:)
もうひとつの追加が、既存の Array に対する mutating メソッドです。要求 capacity を満たすように必要ならバッファを拡張したうえで、初期化済み要素と未初期化領域の両方をカバーする UnsafeMutableBufferPointer と、初期化済み要素数をクロージャに渡します。クロージャは既存要素の変更・破棄や、新規領域の初期化を自由に行え、終了時に initializedCount が最終的な要素数を表すようにします。
public mutating func withUnsafeMutableBufferPointerToStorage<Result>(
capacity: Int,
_ body: (
_ buffer: inout UnsafeMutableBufferPointer<Element>,
_ initializedCount: inout Int
) throws -> Result
) rethrows -> Result
capacity には現在の count 以上の値を渡す必要があり、違反すると実行時エラーになります。これは、配列の中間から要素を切り詰めるような曖昧な操作を防ぐためです。
var a = Array(1...10)
a.withUnsafeMutableBufferPointerToStorage(capacity: 5) { ... } // 実行時エラー
エラー送出時の扱い
クロージャが throw した場合、その時点の initializedCount の値が正しいものとして扱われます。途中で例外を投げたいコードは、投げる前に次のいずれかを満たす必要があります。
- その時点で新たに初期化した要素を deinitialize する、あるいは逆に破棄してしまった既存要素を再初期化する。
initializedCountを現在の実際の初期化済み要素数に更新する。
いずれにしても、「buffer[0..<initializedCount] は初期化済み、buffer[initializedCount...] は未初期化」という事後条件が保たれるようにする責任がユーザー側にあります。
Withdrawn となった経緯
この提案はレビューを経て Withdrawn(Returned for revision) となりました。特に、既存 Array に対する mutating メソッド withUnsafeMutableBufferPointerToStorage(capacity:) のセマンティクスについて、既存要素の扱いや命名をめぐって議論が収束せず、改訂のうえで再提案する方針となりました。
その後、イニシャライザ側のみに絞って改訂された SE-0245: Add an Array Initializer with Access to Uninitialized Storage が Swift 5.1 で採択・実装され、Array(unsafeUninitializedCapacity:initializingWith:) として現在利用できる形になっています。本提案で議論された withUnsafeMutableBufferPointerToStorage(capacity:) に相当するメソッドは、少なくともこの経路では標準ライブラリに入りませんでした。