Swift & Objective-C

[Swift詳解 #6] Swiftのメモリレイアウト:プロパティの順序でサイズが変わる

structのサイズはプロパティサイズの単純な合計ではありません。size・stride・alignment、パディング、extra inhabitantを利用するOptional、64ビットexistential containerの実測を整理します。

読了 11 分
[Swift詳解 #6] Swiftのメモリレイアウト:プロパティの順序でサイズが変わるのカバー画像

ディスパッチ編で「呼び出しがどう変換されるか」を見たなら、今回は「値がどう配置されるか」です。

前回の記事 Swift詳解 #5 の続きです。

structのサイズはプロパティの合計でしょうか。違います。

プロパティの宣言順を変えるとstructのサイズが変わることがある、と聞いたら驚きますか。今回はメモリレイアウト編です。

配列に数十万個のモデルを格納するときや、Instrumentsのメモリグラフが予想以上に大きいときに、この知識が役立ちます。

some·any編で保留した「その箱は実際どれくらい大きいのか」という疑問にも答えます。

3つの寸法 — size、stride、alignment

Swiftには型のメモリ寸法を調べる標準ツールがあります。MemoryLayoutです。

MemoryLayout<Int>.size       // 8
MemoryLayout<Bool>.size      // 1

struct Point { var x: Double; var y: Double }
MemoryLayout<Point>.size     // 16

寸法は3つあり、それぞれ意味が異なります。

sizeは1つの値の連続したメモリフットプリントです。型末尾の整列用tail paddingは含みません。

alignmentは型を置けるアドレスの規則です。CPUは8バイト値を8の倍数のアドレスから読むと効率的で、アーキテクチャによっては必須です。

そのため各型には「nの倍数のアドレスに置く」という制約があります。以下は現行の64ビットAppleプラットフォーム基準で、Doubleのalignmentは8、Boolは1です。

strideは配列の次の要素までの間隔です。整列制約でパディングが生じるため、stride ≥ sizeです。

3つの関係を示す古典的な例がこれです。

struct Bad {
    var flag: Bool      // 1バイト
    var value: Double   // 8バイト — 8の倍数のアドレスに置く必要があります
    var count: Int32    // 4バイト
}
MemoryLayout<Bad>.stride   // 24

struct Good {
    var value: Double   // 8
    var count: Int32    // 4
    var flag: Bool      // 1
}
MemoryLayout<Good>.stride  // 16

内容は同じでも順序を変えると、配列では33%節約できます。Badではflag後のvalueの整列に7バイトのパディングが必要です(Swift ABI Type Layout)。

同じ3つの箱を並べ替え、24バイトを16バイトにする整列比較図
同じ内容でも順序を変えると24が16になる

alignmentの大きいプロパティからまとめるのは、パディングを減らす一般的なヒューリスティックです。ただし常に最適とは限らないため、最終結果はMemoryLayoutで測定します。

現在のSwiftのfragile structレイアウトアルゴリズムは保存プロパティを宣言順に配置します。しかしフィールド順は言語やABIの保証ではありません(Swift ABISE-0260)。正確なオフセットが必要なコードで観測値に依存してはいけません。

この最適化が意味を持つのはインスタンスが大量の場合だけです。設定オブジェクト1個の8バイトとは違い、100万要素の配列では要素あたり8バイトが合計8MBになります(Swift ABI Type Layout)。

いつものように、まず測定しましょう。

enumの魔法 — Optionalのタグが無料になるとき

enumのレイアウトにはSwiftコンパイラの工夫が表れます。ケースのタグを保存する代わりに、ペイロード型の未使用ビットパターン(extra inhabitants)へ隠します。

代表例はBool?です。現行の64ビットAppleプラットフォームではMemoryLayout<Bool?>.sizeは1です。

Boolは1バイト中0と1の2パターンしか使わないため、余ったパターンをnilの表現に使えます。

参照型のOptionalも同じです。Swiftのクラスポインタはnullをオブジェクトに使わないため、nilを0で表します。現行64ビットではAnyObject?のsizeは8です。

すべてのOptionalが無料とは限りません。Intは値の表現に8バイトのビットパターンをすべて使います。そのため現環境でInt?のsizeは9、strideは16です(Swift ABI)。

「安全はほぼ無料」という表現は、余分な表現を再利用できる型では正確です。Optionalという構文自体が、すべての型でメモリコストゼロを保証するわけではありません。

箱を実測する — 単一プロトコルexistentialは5ワード

some·any編では「anyはコストのある箱」とだけ説明しました。ここで実測できます。

protocol Shape {}
MemoryLayout<any Shape>.size   // 40 — 現在の 64ビット環境

クラス制約のない単一プロトコルany Shapeは、現行64ビット環境で40バイト、つまり5ワードです。値バッファ3ワード、型メタデータポインタ、witness tableポインタで構成されます。

重要なのは、格納する値のsizeとalignmentが3ワードのインラインバッファに収まるかどうかです。

条件を超えると値を別領域に割り当て、バッファにはポインタを置きます。ヒープ割り当て、参照カウント、間接参照のコストが追加される可能性があります(Swift ABISE-0335)。

単一プロトコルexistentialの値バッファ3ワードとメタデータ・witness tableの断面図
64ビットの単一非クラスプロトコルexistentialは5ワードで、バッファを超える値は別領域を使います

5ワードはすべてのany型の固定サイズではありません。プロトコル合成ではwitness tableポインタが増えることがあります。

クラス専用プロトコルexistentialは、値バッファと別メタデータポインタを必要としない小さなレイアウトを使います。

プロトコル指向コードが予想外に遅いなら、some·ジェネリックで箱をなくすか、格納する型を小さくする方法を検討できます。効果はInstrumentsとベンチマークで確認してください(SE-0335 Existential any)。

クラス型の値にも触れましょう。現行64ビットAppleプラットフォームでクラス型値のstrideは8で、オブジェクトではなく参照のサイズです。

ネイティブSwiftヒープオブジェクトは通常、メタデータポインタとインライン参照カウント情報を含むヘッダーを持ち、64ビットでは16バイトです。実際の割り当てはプロパティ整列やアロケータの丸めで大きくなることがあります(Swift HeapObjectSwift RefCount)。

ARC編で述べた参照型の隠れたコストは、このヘッダーと割り当て・解放・カウント処理です。小さなデータを数十万個クラス化すると、データより管理コストが大きくなることがあります。

実務ツール箱 — 再測定、確認、疑う

この層を扱う実務ツールを整理します。

**MemoryLayoutで仮説を検証。**大量生成するモデルstructでは、strideを一度測る習慣で十分です。

#expect(MemoryLayout<Tick>.stride <= 32)のようにユニットテストへ予算を設定できます。後からプロパティが増えて超過するとすぐ分かります。

Instruments Allocationsでヒープを確認。「struct中心なのにヒープ割り当てが多い」なら、容疑者は明確です。

anyの箱、クロージャキャプチャ、CoW(copy-on-write)ストレージ、クラスインスタンスです。Allocationsの型別集計が原因を示します。

**copy-on-writeコレクションの錯覚:**現行64ビットAppleプラットフォームでMemoryLayout<[Int]>.strideは8です。Array値はヒープストレージを間接参照する小さなstructですが、この値を固定ABIと仮定してはいけません。

配列を含むstructのstrideが小さいからといって「軽い」と結論できない理由です。

本当の重さはヒープストレージにあり、Allocationsが示します。CoWの原理は別の記事で説明したとおりです。

最後にバランス感覚です。この知識を毎日使うわけではありません。

しかし「なぜ」を知る価値は問題が起きたときに現れます。メモリグラフが異常なとき、推測ではなく容疑者リストから始められる。それが一段深く見た人の利点です。

まとめ

  • 型の寸法は3つです。sizeはtail paddingを除くフットプリント、alignmentはアドレス整列規則、strideは連続配置の間隔です。大量生成するstructではalignmentの大きいプロパティをまとめるとパディングが減ることがありますが、必ず実測してください。
  • enumはextra inhabitantにタグを隠せます。Bool?と参照型Optionalには追加領域がありませんが、Int?のように別のタグ領域が必要な型もあります。
  • 64ビットの単一非クラスプロトコルexistentialは5ワードです。値のsizeまたはalignmentが3ワードのバッファ条件を超えると、別領域と間接コストが発生する可能性があります。
  • 64ビットのネイティブSwiftオブジェクトの基本ヘッダーは通常16バイトです。プロパティとアロケータの整列により実際の割り当ては増えることがあります。
  • MemoryLayoutで設計を検証し、Instruments Allocationsで実測します。測定なしのレイアウト最適化は早すぎる最適化です。

次回はコンパイル時の魔法を扱います。#Previewと@Observableの背後でコードを書く仕組み、Swiftマクロです。

出典と確認基準

  • MemoryLayout — Apple Developer Documentation · 確認 2026-08-19 · 根拠:size・alignment・strideのAPI定義
  • Swift ABI Type Layout — Swiftプロジェクト · 公式文書 · 確認 2026-08-19 · 根拠:struct・enum・existential containerのレイアウト
  • SE-0260 Library Evolution — Swift Evolution · 確認 2026-08-19 · 根拠:structフィールド順の言語・ABI保証範囲
  • SE-0335 Existential any — Swift Evolution · 確認 2026-08-19 · 根拠:existentialの3ワードインラインバッファと動的メモリコスト
  • Swift HeapObject · Swift RefCount — Swift Runtime · 確認 2026-08-19 · 根拠:ネイティブヒープオブジェクトのヘッダーとインライン参照カウント

あわせて読みたい

Swift詳解シリーズ