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 ABI · SE-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バイト中2つのパターン(0、1)しか使わないため、残りのパターンをnilの表現に使います。

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

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

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

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

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

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

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

重要な分岐は、格納する値のsizeとalignmentがともに3ワードのインラインバッファ条件を満たすかどうかです。

条件を超えると値を別の保存領域に割り当て、バッファにはポインタを置きます。この場合、ヒープ割り当て、参照カウント、間接参照のコストが加わる可能性があります(Swift ABI · SE-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 HeapObject · Swift 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を除くfootprint、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ランタイム · 確認日 2026-08-19 · 根拠:ネイティブヒープオブジェクトのヘッダーとインライン参照カウント

あわせて読みたい