🚀

SwiftのStringのAllocationsの最適化とストレージ選択の仕組み

に公開

はじめに

try! Swift Tokyo2026のPaul HudsonさんのワークショップのHigh-Performance Swiftに参加し、Swift言語のStringStorageの話を聞き、Stringの内部構造とどのように最適化されているのかについて興味を持ったので、この記事を書きました。
https://x.com/hinakkograshi/status/2043153057278763029?s=20

SwiftのStringのAllocations

Stringのストレージの格納場所は大きく分けて、Small String Optimization__StringStorage__SharedStringStorageの3種類あり、SwiftのStringは状況に応じてストレージ戦略を切り替えています。それぞれについて詳しくみていきましょう。

Small String Optimization

64ビットシステムでは最大15バイトまでの短い文字列は、_StringObjectの中に直接インライン格納されます。
https://github.com/swiftlang/swift/blob/main/stdlib/public/core/SmallString.swift#L82-L93
_StringObjectの16バイトの中に文字データを直接インライン格納するため、文字データのためのヒープアロケーションが発生せず、アクセスが非常に高速です。

let hello = "Hello, World!" // 13バイト

__StringStorage

16バイト以上の文字列はヒープ上に __StringStorageを確保します。SwiftのStringは値型でありながら、内部でヒープ上のストレージへの参照を持ちます。
__StringStorageはclassで実装されています。
https://github.com/swiftlang/swift/blob/main/stdlib/public/core/StringStorage.swift#L274-L278
値型なのでコピーが発生するように見えますが、毎回ヒープを確保してコピーすると非常に遅くなります。そのためSwiftではCopy-on-Write(CoW)という仕組みが使われます。CoWとは、実際に値に変更が加わるまでコピーしない最適化のことで、複数の変数が同じストレージを参照し、変更が発生した時点で初めてコピーします。不必要なコピーを避けることでメモリ使用量と処理速度を最適化できます。

Copy-on-Write(CoW)について詳しくはこちら

AppleのAPIではStringArrayDictionary等で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
    }
  }
}

https://github.com/swiftlang/swift/blob/main/docs/OptimizationTips.rst#advice-use-copy-on-write-semantics-for-large-values

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)になり、コストを抑えることができます。

https://github.com/swiftlang/swift/blob/main/stdlib/public/core/StringStorage.swift#L158-L165

__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との違いは、文字データをオブジェクト自身が所有するのではなく、_ownerstartポインタで外部バッファを参照する点です。
https://github.com/swiftlang/swift/blob/main/stdlib/public/core/StringStorage.swift#L754-L758

SwiftのStringのストレージ選択の仕組み

Swiftネイティブな文字列の動的生成の場合、Stringをインスタンス化する際にStringCreate.swift_uncheckedFromASCIIまたは _uncheckedFromUTF8が実行されます。
①まずSmallString(15バイト)に収まるか試みる → 収まった場合、Small String Optimizationヒープなしで割り当てられる。
②収まらなければ__StringStorageをヒープに確保し、割り当てられる。
https://github.com/swiftlang/swift/blob/main/stdlib/public/core/StringCreate.swift#L138-L149
https://github.com/swiftlang/swift/blob/main/stdlib/public/core/StringCreate.swift#L266-L279

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に割り当てられます。
https://github.com/swiftlang/swift/blob/main/stdlib/public/core/StringBridge.swift#L507-L547

おわりに

Paul Hudsonさんのワークショップをきっかけに、普段は意識しない標準ライブラリのコードを読むという体験ができました。ソースコードを読む前は「Stringは値型だからコピーされる」という表面的な理解しかありませんでしたが、実際にコードを追うことで、15バイト未満ならヒープアロケーションすら発生しないSmall String Optimization、CoWやテールアロケーションで効率化された__StringStorage、外部バッファをコピーせず参照し続ける__SharedStringStorageと、状況に応じてストレージ戦略が最適化される仕組みが見えて面白かったです。

Discussion