SwiftのStringのAllocationsの最適化とストレージ選択の仕組み
はじめに
try! Swift Tokyo2026のPaul HudsonさんのワークショップのHigh-Performance Swiftに参加し、Swift言語のStringStorageの話を聞き、Stringの内部構造とどのように最適化されているのかについて興味を持ったので、この記事を書きました。
SwiftのStringのAllocations
Stringのストレージの格納場所は大きく分けて、Small String Optimization・__StringStorage・__SharedStringStorageの3種類あり、SwiftのStringは状況に応じてストレージ戦略を切り替えています。それぞれについて詳しくみていきましょう。
Small String Optimization
64ビットシステムでは最大15バイトまでの短い文字列は、_StringObjectの中に直接インライン格納されます。
_StringObjectの16バイトの中に文字データを直接インライン格納するため、文字データのためのヒープアロケーションが発生せず、アクセスが非常に高速です。
let hello = "Hello, World!" // 13バイト
__StringStorage
16バイト以上の文字列はヒープ上に __StringStorageを確保します。SwiftのStringは値型でありながら、内部でヒープ上のストレージへの参照を持ちます。
__StringStorageはclassで実装されています。
値型なのでコピーが発生するように見えますが、毎回ヒープを確保してコピーすると非常に遅くなります。そのためSwiftではCopy-on-Write(CoW)という仕組みが使われます。CoWとは、実際に値に変更が加わるまでコピーしない最適化のことで、複数の変数が同じストレージを参照し、変更が発生した時点で初めてコピーします。不必要なコピーを避けることでメモリ使用量と処理速度を最適化できます。
Copy-on-Write(CoW)について詳しくはこちら
AppleのAPIではString、Array、Dictionary等でCopy-on-Writeの仕組みは使用されています。
下記のようにデータ構造を作成することで、大きなデータを持つ構造体において、Copy-on-Writeの仕組みを活用することができます。
final class Ref<T> {
var val: T
init(_ v: T) { val = v }
}
struct Box<T> {
var ref: Ref<T>
init(_ x: T) { ref = Ref(x) }
var value: T {
get { return ref.val }
set {
if !isKnownUniquelyReferenced(&ref) {
ref = Ref(newValue)
return
}
ref.val = newValue
}
}
}
let a = "This is over fifteen bytes!!"// __StringStorage(参照カウント1)
var b = a // __StringStorage(参照カウント2)コピーなし・同じストレージを参照
b += " more" // __StringStorageを新たにコピーしてから新しいヒープ領域に割り当てる
また、__StringStorageはテールアロケーション(オブジェクトヘッダの直後に文字データが連続配置)という構造を取ります。
通常のヒープオブジェクトはオブジェクト本体とデータが別々のメモリ領域に確保されポインタで繋がれますが、テールアロケーションではヘッダから固定オフセットで直接文字データにアクセスできるためポインタ経由の間接参照が不要になり、メモリアクセスが高速です。
また、__StringStorageは余剰容量(spare code unit capacity)を持ちます。そのため、通常であればappendのたびに毎回ヒープを再確保すると O(n)になりますが、余剰容量内に収まる場合はヒープの再確保が不要なのでO(1)になり、コストを抑えることができます。
__SharedStringStorage
SwiftのリテラルStringをNSStringに変換し、さらにStringに戻す場合に使われるストレージです。文字データをコピーせず、外部のバッファをそのまま参照し続けます。
let s = "This is over fifteen bytes!!" // バイナリの定数セクションに格納(immortal)
let ns = s as NSString // __SharedStringStorage作成
let back = ns as String // __SharedStringStorage をそのまま使う(コピーなし)
__StringStorageとの違いは、文字データをオブジェクト自身が所有するのではなく、_ownerとstartポインタで外部バッファを参照する点です。
SwiftのStringのストレージ選択の仕組み
Swiftネイティブな文字列の動的生成の場合、Stringをインスタンス化する際にStringCreate.swiftの_uncheckedFromASCIIまたは _uncheckedFromUTF8が実行されます。
①まずSmallString(15バイト)に収まるか試みる → 収まった場合、Small String Optimizationヒープなしで割り当てられる。
②収まらなければ__StringStorageをヒープに確保し、割り当てられる。
NSStringがSwift Stringに変換される場合、StringBridge.swiftの_bridgeCocoaStringメソッドが実行され、NSStringの種類に応じて以下の4つに分岐します。
.storage:すでに__StringStorageであればそのまま使用
.shared:__SharedStringStorageであればそのまま使用(SwiftリテラルのNSString往復がこれに該当)
.tagged:小さいNSStringはSmall String(SSO)に変換
.cocoa:その他のNSStringはCocoaオブジェクトとして保持
SwiftのリテラルStringをNSStringに変換し、さらにStringに戻す場合、sharedケースになり、__SharedStringStorageに割り当てられます。
おわりに
Paul Hudsonさんのワークショップをきっかけに、普段は意識しない標準ライブラリのコードを読むという体験ができました。ソースコードを読む前は「Stringは値型だからコピーされる」という表面的な理解しかありませんでしたが、実際にコードを追うことで、15バイト未満ならヒープアロケーションすら発生しないSmall String Optimization、CoWやテールアロケーションで効率化された__StringStorage、外部バッファをコピーせず参照し続ける__SharedStringStorageと、状況に応じてストレージ戦略が最適化される仕組みが見えて面白かったです。
Discussion