axiom-swift-performance について
値セマンティクス、ARC、ジェネリクス、メモリレイアウトなど、Swift の言語レベルのコストを解説する性能最適化ガイドです。プロファイリングで Swift コードがボトルネックと判明した際の改善に役立ちます。
主な機能
- 01値型と参照型の使い分け
- 02非コピー型と COW の解説
- 03ARC とクロージャのコスト
- 04特殊化とインライン化の説明
- 05コレクションとメモリ配置の最適化
活用シーン
- 01プロファイリングで特定した Swift のホットスポットを最適化
- 02大きなデータ構造のコピーを削減
- 03頻繁に実行する処理の ARC と割り当てを抑制
- 04固定サイズコレクションの保存方式を選択
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-swift-performanceインストール後、Claude Code で/axiom-swift-performanceを入力して読み込まれたか確認します。
詳細
Swift パフォーマンス最適化
目的
基本原則:言語レベルのパフォーマンス特性(値セマンティクス、ARC の動作、ジェネリックの特殊化、メモリレイアウト)を理解して Swift コードを最適化し、早まったマイクロ最適化をせずに、高速で効率的なコードを書きます。
Swift バージョン:Swift 6.2+(InlineArray、Span、@concurrent 用)
Xcode:16+
プラットフォーム:iOS 18+、macOS 15+
関連スキル:
axiom-performance-profiling— Instruments を使った測定(最初に実施してください!)axiom-swiftui-performance— SwiftUI 固有の最適化axiom-build-performance— コンパイル速度axiom-swift-concurrency— 正確性を重視した並行処理パターン
このスキルを使う場面
✅ このスキルを使う場合
- アプリのプロファイリングで Swift コードがボトルネックと判明した場合(Time Profiler のホットスポット)
- メモリの過剰な割り当て、または retain/release の頻発
- パフォーマンスが重要なアルゴリズムやデータ構造の実装
- パフォーマンス要件のあるフレームワークやライブラリのコードを書く場合
- タイトなループや頻繁に呼び出されるメソッドの最適化
- 大規模なデータ構造やコレクションを扱う場合
- コードレビューでパフォーマンスのアンチパターンが特定された場合
クイック判断ツリー
Performance issue identified?
│
├─ Profiler shows excessive copying?
│ └─ → Part 1: Noncopyable Types
│ └─ → Part 2: Copy-on-Write
│
├─ Retain/release overhead in Time Profiler?
│ └─ → Part 4: ARC Optimization
│
├─ Generic code in hot path?
│ └─ → Part 5: Generics & Specialization
│
├─ Collection operations slow?
│ └─ → Part 7: Collection Performance
│
├─ Async/await overhead visible?
│ └─ → Part 8: Concurrency Performance
│
├─ Struct vs class decision?
│ └─ → Part 3: Value vs Reference
│
└─ Memory layout concerns?
└─ → Part 9: Memory Layout
Swift パフォーマンスの 4 つの原則
WWDC 2024-10217 によると、Swift の低レベルなパフォーマンス特性は 4 つの領域に集約されます。それぞれがこのスキルの各パートに対応しています。
| 原則 | コストの内容 | 対応するパート |
|---|---|---|
| 関数呼び出し | ディスパッチのオーバーヘッド、最適化の障壁 | パート 5(ジェネリクス)、パート 6(インライン化) |
| メモリ割り当て | スタックとヒープ、割り当て頻度 | パート 3(値と参照)、パート 7(コレクション) |
| メモリレイアウト | キャッシュ局所性、パディング、連続性 | パート 9(メモリレイアウト)、パート 11(Span) |
| 値のコピー | COW の発生、防御的コピー、ARC のトラフィック | パート 1(非コピー可能型)、パート 2(COW)、パート 4(ARC) |
どの原則がボトルネックの原因かを理解することで、使用すべきパートが決まります。
パート 1:非コピー可能型(~Copyable)
Swift 6.0+ では、暗黙的なコピーを避けたいパフォーマンス重視の状況で使える非コピー可能型が導入されました。
使用する場面
- コピーすべきでない大きな型(ファイルハンドル、GPU バッファー)
- 所有権セマンティクスを持つ型(明示的に消費する必要がある)
- コピーのコストが高いパフォーマンス重視のコード
基本パターン
// Noncopyable type
struct FileHandle: ~Copyable {
private let fd: Int32
init(path: String) throws {
self.fd = open(path, O_RDONLY)
guard fd != -1 else { throw FileError.openFailed }
}
deinit {
close(fd)
}
// Must explicitly consume
consuming func close() {
_ = consume self
}
}
// Usage
func processFile() throws {
let handle = try FileHandle(path: "/data.txt")
// handle is automatically consumed at end of scope
// Cannot accidentally copy handle
}
所有権アノテーション
// consuming - takes ownership, caller cannot use after
func process(consuming data: [UInt8]) {
// data is consumed
}
// borrowing - temporary access without ownership
func validate(borrowing data: [UInt8]) -> Bool {
// data can still be used by caller
return data.count > 0
}
// inout - mutable access
func modify(inout data: [UInt8]) {
data.append(0)
}
パフォーマンスへの影響
- 暗黙的なコピーを排除:実行時のコピーではなく、コンパイラーエラーになります
- ゼロコスト抽象化:手動でのメモリ管理と同等のパフォーマンスです
- 使用する場面:型のコピーコストが高く(64 バイト超)、コピーの頻度が低い場合
パート 2:コピーオンライト(COW)
Swift のコレクションは、効率的なメモリ共有のために COW を使用します。コピーが発生するタイミングを理解することは、パフォーマンス上重要です。
COW の仕組み
var array1 = [1, 2, 3] // Single allocation
var array2 = array1 // Share storage (no copy)
array2.append(4) // Now copies (array1 modified array2)
カスタム COW 実装については、以下のコピー&ペーストパターン 1(COW ラッパー)を参照してください。
パフォーマンスのヒント
// ❌ Accidental copy in loop
for i in 0..<array.count {
array[i] = transform(array[i]) // Copy on first mutation if shared!
}
// ✅ Reserve capacity first (ensures unique)
array.reserveCapacity(array.count)
for i in 0..<array.count {
array[i] = transform(array[i])
}
// ❌ Multiple mutations trigger multiple uniqueness checks
array.append(1)
array.append(2)
array.append(3)
// ✅ Single reservation
array.reserveCapacity(array.count + 3)
array.append(contentsOf: [1, 2, 3])
防御的コピー
WWDC 2024-10217 によると、Swift は共有参照を通じて値が変更されないことを証明できない場合に、防御的コピーを挿入することがあります。
class DataStore {
var items: [Item] = [] // COW type stored in class
}
func process(_ store: DataStore) {
for item in store.items {
// Swift may defensively copy `items` because:
// 1. store.items is a class property (another reference could mutate it)
// 2. The loop needs a stable snapshot
handle(item)
}
}
回避方法:まずローカル変数にコピーします。防御的コピーが繰り返し行われる代わりに、明示的なコピーを 1 回行います。
func process(_ store: DataStore) {
let items = store.items // One copy
for item in items {
handle(item) // No more defensive copies
}
}
プロファイラーでの確認:防御的コピーは、割り当てが発生するとは思わなかった箇所で、予期しない swift_retain/swift_release のペアや Array.__allocating_init の呼び出しとして現れます。
パート 3:値セマンティクスと参照セマンティクス
struct と class の選択は、パフォーマンスに大きく影響します。
判断基準
| 要素 | Struct を使う | Class を使う |
|---|---|---|
| サイズ | ≤ 64 バイト | > 64 バイト、または大きなデータを含む |
| 識別性 | 識別性が不要 | 識別性(===)が必要 |
| 継承 | 不要 | 継承が必要 |
| 変更 | 頻繁には変更しない | インプレース更新を頻繁に行う |
| 共有 | 共有不要 | スコープをまたいだ共有が必要 |
小さな構造体(高速)
// ✅ Fast - fits in registers, no heap allocation
struct Point {
var x: Double // 8 bytes
var y: Double // 8 bytes
} // Total: 16 bytes - excellent for struct
struct Color {
var r, g, b, a: UInt8 // 4 bytes total - perfect for struct
}
大きな構造体(低速)
// ❌ Slow - excessive copying
struct HugeData {
var buffer: [UInt8] // 1MB
var metadata: String
}
func process(_ data: HugeData) { // Copies 1MB!
// ...
}
// ✅ Use reference semantics for large data
final class HugeData {
var buffer: [UInt8]
var metadata: String
}
func process(_ data: HugeData) { // Only copies pointer (8 bytes)
// ...
}
柔軟性のための間接ストレージ
内部では参照ストレージを使用しつつ、外部では値セマンティクスが必要な大きなデータには、COW ラッパーパターンを使用してください。下記のコピーペーストパターン 1 を参照してください。
パート 4:ARC の最適化
自動参照カウントにはオーバーヘッドが伴います。可能な限り最小限に抑えてください。
Weak と Unowned のパフォーマンス
class Parent {
var child: Child?
}
class Child {
// ❌ Weak adds overhead (optional, thread-safe zeroing)
weak var parent: Parent?
}
// ✅ Unowned when you know lifetime guarantees
class Child {
unowned let parent: Parent // No overhead, crashes if parent deallocated
}
パフォーマンス:unowned は weak より約 2 倍高速です(アトミック操作がありません)。
使用する場合:子のライフタイムが親のライフタイムより短いことが保証されている場合です。
クロージャのキャプチャ最適化
class DataProcessor {
var data: [Int]
// ❌ Captures self strongly, then uses weak - unnecessary weak overhead
func process(completion: @escaping () -> Void) {
DispatchQueue.global().async { [weak self] in
guard let self else { return }
self.data.forEach { print($0) }
completion()
}
}
// ✅ Capture only what you need
func process(completion: @escaping () -> Void) {
let data = self.data // Copy value type
DispatchQueue.global().async {
data.forEach { print($0) } // No self captured
completion()
}
}
}
クロージャのキャプチャコスト
WWDC 2024-10217 より:クロージャのパフォーマンス特性は、エスケープするかどうかによって異なります。
// Non-escaping closure — stack-allocated context, zero ARC overhead
func processItems(_ items: [Item], using transform: (Item) -> Result) -> [Result] {
items.map(transform) // Closure context lives on stack
}
// Escaping closure — heap-allocated context, ARC on every captured reference
func processItemsLater(_ items: [Item], transform: @escaping (Item) -> Result) {
// Closure context heap-allocated as anonymous class instance
// Each captured reference gets retain/release
self.pending = { items.map(transform) }
}
重要な理由:@Sendable クロージャは常にエスケープするため、すべての Task クロージャでキャプチャコンテキストがヒープに割り当てられます。
ホットパスでは:非エスケープクロージャを優先してください。Time Profiler でクロージャのコンテキストに swift_allocObject が表示された場合は、非エスケープにできるエスケープクロージャがないか確認してください。
Observable オブジェクトのライフタイム
WWDC 2021-10216 より:オブジェクトのライフタイムが終了するのは、閉じ波括弧ではなく、最後に使用した時点です。
// ❌ Relying on observed lifetime is fragile
class Traveler {
weak var account: Account?
deinit {
print("Deinitialized") // May run BEFORE expected with ARC optimizations!
}
}
func test() {
let traveler = Traveler()
let account = Account(traveler: traveler)
// traveler's last use is above - may deallocate here!
account.printSummary() // weak reference may be nil!
}
// ✅ Explicitly extend lifetime when needed
func test() {
let traveler = Traveler()
let account = Account(traveler: traveler)
withExtendedLifetime(traveler) {
account.printSummary() // traveler guaranteed to live
}
}
オブジェクトのライフタイムは、Xcode のバージョン、Debug と Release の違い、無関係なコード変更によって変わることがあります。開発中に「Optimize Object Lifetimes」(Xcode 13 以降)を有効にすると、隠れたライフタイムの不具合を早期に見つけられます。
パート 5:ジェネリクスと特殊化
ジェネリックコードのパフォーマンスは、特殊化の有無によって変わります。
特殊化の基本
// Generic function
func process<T>(_ value: T) {
print(value)
}
// Calling with concrete type
process(42) // Compiler specializes: process_Int(42)
process("hello") // Compiler specializes: process_String("hello")
Existential のオーバーヘッド
protocol Drawable {
func draw()
}
// ❌ Existential container - expensive (heap allocation, indirection)
func drawAll(shapes: [any Drawable]) {
for shape in shapes {
shape.draw() // Dynamic dispatch through witness table
}
}
// ✅ Generic with constraint - can specialize
func drawAll<T: Drawable>(shapes: [T]) {
for shape in shapes {
shape.draw() // Static dispatch after specialization
}
}
パフォーマンス:ジェネリック版は約 10 倍高速です(witness table のオーバーヘッドを排除します)。
Existential コンテナのオーバーヘッド
WWDC 2016-416 より:any Protocol は 40 バイトの existential コンテナ(64 ビットで 5 ワード)を使用します。このコンテナには、型メタデータとプロトコル witness table(16 バイト)、および 24 バイトのインライン値バッファが格納されます。24 バイト以下の型はバッファに直接格納され(高速で、アクセスは約 5 ns)、それより大きな型ではポインター間接参照を伴うヒープ割り当てが必要になります(低速で、約 15 ns)。some Protocol ではコンテナのオーバーヘッドがすべてなくなります(約 2 ns)。
some を使用できない場合(異種コレクションには any が必要です):
- 型のサイズを 24 バイト以下に抑える — プロトコルに準拠する型をインラインストレージに収まる大きさに保ちます(3 ワード。例:
Point { x, y, z: Double }はちょうど収まります) - 代わりに enum によるディスパッチを使用する — コンテナを完全に排除し、オープンな拡張性と引き換えにパフォーマンスを向上させます:
// ❌ Existential: 40 bytes/element, witness table dispatch
let shapes: [any Drawable] = [circle, rect]
// ✅ Enum: value-sized, static dispatch via switch
enum Shape { case circle(Circle), rect(Rect) }
func draw(_ shape: Shape) {
switch shape {
case .circle(let c): c.draw()
case .rect(let r): r.draw()
}
}
- 操作をまとめて実行する — 1 件ずつ処理するのではなく、チャンク単位で処理し、要素ごとの existential オーバーヘッドを分散させます
- まず測定する — existential のオーバーヘッド(アクセスあたり約 10 ns)が問題になるのはタイトなループ内だけです。UI レベルのコードでは無視できる程度です
@_specialize 属性
コンパイラーが自動的に特殊化しない場合は、よく使われる型に対して特殊化を強制します:
@_specialize(where T == Int)
@_specialize(where T == String)
func process<T: Comparable>(_ value: T) -> T { value }
// Generates specialized versions + generic fallback
パート 6:インライン化
インライン化によって関数呼び出しのオーバーヘッドを排除できますが、コードサイズは大きくなります。
インライン化する場合
// ✅ Small, frequently called functions
@inlinable
public func fastAdd(_ a: Int, _ b: Int) -> Int {
return a + b
}
// ❌ Large functions - code bloat
@inlinable // Don't do this!
public func complexAlgorithm() {
// 100 lines of code...
}
モジュール間の最適化
// Framework code
public struct Point {
public var x: Double
public var y: Double
// ✅ Inlinable for cross-module optimization
@inlinable
public func distance(to other: Point) -> Double {
let dx = x - other.x
let dy = y - other.y
return sqrt(dx*dx + dy*dy)
}
}
// Client code
let p1 = Point(x: 0, y: 0)
let p2 = Point(x: 3, y: 4)
let d = p1.distance(to: p2) // Inlined across module boundary
@usableFromInline
// Internal helper that can be inlined
@usableFromInline
internal func helperFunction() { }
// Public API that uses it
@inlinable
public func publicAPI() {
helperFunction() // Can inline internal function
}
トレードオフ:@inlinable は実装を公開し、将来の最適化を妨げます。
パート 7:コレクションのパフォーマンス
適切なコレクションを選び、正しく使用することが重要です。
Array と ContiguousArray
// ❌ Array<T> - may use NSArray bridging (Swift/ObjC interop)
let array: Array<Int> = [1, 2, 3]
// ✅ ContiguousArray<T> - guaranteed contiguous memory (no bridging)
let array: ContiguousArray<Int> = [1, 2, 3]
ContiguousArray を使用する場合:ObjC ブリッジが不要な場合(純粋な Swift)で、約 15%高速になります。
容量の予約
// ❌ Multiple reallocations
var array: [Int] = []
for i in 0..<10000 {
array.append(i) // Reallocates ~14 times
}
// ✅ Single allocation
var array: [Int] = []
array.reserveCapacity(10000)
for i in 0..<10000 {
array.append(i) // No reallocations
}
Dictionary のハッシュ化
struct BadKey: Hashable {
var data: [Int]
// ❌ Expensive hash (iterates entire array)
func hash(into hasher: inout Hasher) {
for element in data {
hasher.combine(element)
}
}
}
struct GoodKey: Hashable {
var id: UUID // Fast hash
var data: [Int] // Not hashed
// ✅ Hash only the unique identifier
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
InlineArray(Swift 6.2)
スタック上に直接格納される固定サイズ配列です。ヒープ割り当ても、COW のオーバーヘッドもありません。値ジェネリクスを使用して、型にサイズをエンコードします。
// Traditional Array - heap allocated, COW overhead
var sprites: [Sprite] = Array(repeating: .default, count: 40)
// InlineArray - stack allocated, no COW (value generic syntax)
var sprites = InlineArray<40, Sprite>(repeating: .default)
準拠:RandomAccessCollection、MutableCollection、BitwiseCopyable、Sendable。~Copyable の要素型をサポートします。
InlineArray を使用する場面:
- コンパイル時にサイズが決まっている
- パフォーマンスが重要な処理(タイトなループ、ホットパス)
- ヒープ割り当てを完全に避けたい
- 小〜中サイズ(実用上の上限はスタック使用量約1KB)
InlineArray はスタックに割り当てられ(ヒープは使用しません)、即時にコピーされ(COW ではありません)、ゼロコピーアクセス用の .span/.mutableSpan を提供します。Array と比較した割り当て/コピー/変更のトレードオフについては、独自のベンチマークで測定してください。
コピーセマンティクスに関する警告:
// ❌ Unexpected: InlineArray copies eagerly
func processLarge(_ data: InlineArray<1000, UInt8>) {
// Copies all 1000 bytes on call!
}
// ✅ Use Span to avoid copy
func processLarge(_ data: Span<UInt8>) {
// Zero-copy view, no matter the size
}
// Best practice: Store InlineArray, pass Span
struct Buffer {
var storage = InlineArray<1000, UInt8>(repeating: 0)
func process() {
helper(storage.span) // Pass view, not copy
}
}
InlineArray を使用しない場面:
- サイズが動的に変わる(Array を使用)
- 大きなデータ(スタック使用量が1KBを超えるとリスクがあります)
- 値渡しを頻繁に行う(代わりに Span を使用)
- COW セマンティクスが必要(Array を使用)
遅延シーケンス
// ❌ Eager evaluation - processes entire array
let result = array
.map { expensive($0) }
.filter { $0 > 0 }
.first // Only need first element!
// ✅ Lazy evaluation - stops at first match
let result = array
.lazy
.map { expensive($0) }
.filter { $0 > 0 }
.first // Only evaluates until first match
パート8:並行処理のパフォーマンス
async/await とアクターにはオーバーヘッドがあります。適切に使用してください。
アクター隔離のオーバーヘッド
actor Counter {
private var value = 0
// ❌ Actor call overhead for simple operation
func increment() {
value += 1
}
}
// Calling from different isolation domain
for _ in 0..<10000 {
await counter.increment() // 10,000 actor hops!
}
// ✅ Batch operations to reduce actor overhead
actor Counter {
private var value = 0
func incrementBatch(_ count: Int) {
value += count
}
}
await counter.incrementBatch(10000) // Single actor hop
Async のオーバーヘッド
各 async サスペンションには約20~30μsのコストがかかります。同期処理は同期処理のままにしてください。await が不要な関数に async を付けないでください。
タスク作成のコスト
// ❌ Creating task per item (~100μs overhead each)
for item in items {
Task {
await process(item)
}
}
// ✅ Single task for batch
Task {
for item in items {
await process(item)
}
}
// ✅ Or use TaskGroup for parallelism
await withTaskGroup(of: Void.self) { group in
for item in items {
group.addTask {
await process(item)
}
}
}
@concurrent 属性(Swift 6.2)
// Force background execution
@concurrent
func expensiveComputation() -> Int {
// Always runs on background thread, even if called from MainActor
return complexCalculation()
}
// Safe to call from main actor without blocking
@MainActor
func updateUI() async {
let result = await expensiveComputation() // Guaranteed off main thread
label.text = "\(result)"
}
nonisolated のパフォーマンスパターンと、アクター隔離に関する詳細なガイダンスについては、axiom-swift-concurrency を参照してください。
パート9:メモリレイアウト
メモリレイアウトを理解すると、キャッシュのパフォーマンス向上や割り当ての削減に役立ちます。
構造体のパディング
// ❌ Poor layout (24 bytes due to padding)
struct BadLayout {
var a: Bool // 1 byte + 7 padding
var b: Int64 // 8 bytes
var c: Bool // 1 byte + 7 padding
}
print(MemoryLayout<BadLayout>.size) // 24 bytes
// ✅ Optimized layout (16 bytes)
struct GoodLayout {
var b: Int64 // 8 bytes
var a: Bool // 1 byte
var c: Bool // 1 byte + 6 padding
}
print(MemoryLayout<GoodLayout>.size) // 16 bytes
アライメント
// Query alignment
print(MemoryLayout<Double>.alignment) // 8
print(MemoryLayout<Int32>.alignment) // 4
// Structs align to largest member
struct Mixed {
var int32: Int32 // 4 bytes, 4-byte aligned
var double: Double // 8 bytes, 8-byte aligned
}
print(MemoryLayout<Mixed>.alignment) // 8 (largest member)
キャッシュフレンドリーなデータ構造
// ❌ Poor cache locality
struct PointerBased {
var next: UnsafeMutablePointer<Node>? // Pointer chasing
}
// ✅ Array-based for cache locality
struct ArrayBased {
var data: ContiguousArray<Int> // Contiguous memory
}
// Array iteration ~10x faster due to cache prefetching
排他性チェック
WWDC 2025-312より:コンパイラがメモリ安全性を静的に証明できない場合、実行時の排他性チェック(swift_beginAccess/swift_endAccess)が Time Profiler に表示されます。
概要:Swift では、一方が書き込みの場合、同じ変数への2つのアクセスが重複しないように強制されます。構造体のプロパティではコンパイル時にチェックされます。クラスの格納プロパティでは、実行時チェックが挿入されます。
確認方法:Time Profiler または Processor Trace のフレームグラフで swift_beginAccess と swift_endAccess を探してください。
// ❌ Class properties require runtime exclusivity checks
class Parser {
var state: ParserState
var cache: [Int: Pixel]
func parse() {
state.advance() // swift_beginAccess / swift_endAccess
cache[key] = pixel // swift_beginAccess / swift_endAccess
}
}
// ✅ Struct properties checked at compile time — zero runtime cost
struct Parser {
var state: ParserState
var cache: InlineArray<64, Pixel>
mutating func parse() {
state.advance() // No runtime check
cache[key] = pixel // No runtime check
}
}
実環境での効果:WWDC 2025-312 の QOI 画像パーサーでは、プロパティをクラスから構造体に移したことで、すべての実行時排他性チェックがなくなりました。これにより、合計で700倍を超える改善の一環として、測定可能な速度向上が得られました。
パート10:型付き throws(Swift 6)
型付き throws は、存在型のオーバーヘッドを避けられるため、型指定なしの throws より高速になる場合があります。
型指定なしと型付きの比較
// Untyped - existential container for error
func fetchData() throws -> Data {
// Can throw any Error
throw NetworkError.timeout
}
// Typed - concrete error type
func fetchData() throws(NetworkError) -> Data {
// Can only throw NetworkError
throw NetworkError.timeout
}
パフォーマンスへの影響
// Measure with tight loop
func untypedThrows() throws -> Int {
throw GenericError.failed
}
func typedThrows() throws(GenericError) -> Int {
throw GenericError.failed
}
// Benchmark: typed ~5-10% faster (no existential overhead)
使用する場面
- 型付き:エラー型が明確に定義されたライブラリコード、ホットパス
- 型指定なし:アプリケーションコード、コンパイル時にエラー型が不明な場合
パート11:Span 型
Swift 6.2 以降では、Span が導入されています。これは、メモリを所有せず、エスケープできないビューであり、連続データへの安全で効率的なアクセスを提供します。
Span とは?
Span は UnsafeBufferPointer に代わる最新の型で、次の特性があります。
- 空間安全性:境界チェック付きの操作により、範囲外アクセスを防止します
- 時間安全性:ソースからライフタイムを継承し、解放後の使用を防止します
- オーバーヘッドなし:ヒープ割り当ても参照カウントもありません
- エスケープ不可:参照先のデータより長く存続することはできません
// Traditional unsafe approach
func processUnsafe(_ data: UnsafeMutableBufferPointer<UInt8>) {
data[100] = 0 // Crashes if out of bounds!
}
// Safe Span approach
func processSafe(_ data: MutableSpan<UInt8>) {
data[100] = 0 // Traps with clear error if out of bounds
}
Span、Array、UnsafeBufferPointer の使い分け
| 使用例 | 推奨 |
|---|---|
| データを所有する | Array(完全な所有権、COW) |
| 読み取り用の一時的なビュー | Span(安全、高速) |
| 書き込み用の一時的なビュー | MutableSpan(安全、高速) |
| C との相互運用、パフォーマンスが重要な処理 | RawSpan(型なしバイト列) |
| 安全性を犠牲にしたパフォーマンス | UnsafeBufferPointer(レガシー、非推奨) |
Span の基本的な使い方
let array = [1, 2, 3, 4, 5]
let span = array.span // Read-only view
print(span[0]) // Subscript access
for element in span { } // Safe iteration
let slice = span[1..<3] // Span slice, no copy
変更に使用する MutableSpan
var array = [10, 20, 30, 40, 50]
let mutableSpan = array.mutableSpan
mutableSpan[0] = 100 // Modifies array in-place, bounds-checked
型付けされていないバイト列のための RawSpan
func parsePacket(_ data: RawSpan) -> PacketHeader? {
guard data.count >= MemoryLayout<PacketHeader>.size else { return nil }
// Safe byte-level access via subscript
return PacketHeader(version: data[0], flags: data[1],
length: UInt16(data[3]) << 8 | UInt16(data[2]))
}
let header = parsePacket(bytes.rawSpan) // .rawSpan on any [UInt8]
Swift 6.2 のすべてのコレクションには、Array、ContiguousArray、UnsafeBufferPointer(移行パス)などを含め、.span と .mutableSpan のプロパティが用意されています。Span のアクセス速度は、境界チェックを行いながらも UnsafeBufferPointer と同等です(約 2ns)。
非エスケープによるライフタイムの安全性
Span のライフタイムは、そのソースに紐付けられています。ソースが解放されてしまう関数から Span を返そうとすると、コンパイラーがそれを防ぎます。このようなバグを黙って許してしまう UnsafeBufferPointer とは異なります。
func dangerousSpan() -> Span<Int> {
let array = [1, 2, 3]
return array.span // ❌ Error: Cannot return non-escapable value
}
InlineArray にも .span/.mutableSpan が用意されています。InlineArray の使い方と、Span によるコピー回避については、パート 7 を参照してください。
UnsafeBufferPointer からの移行
// ❌ Old: unsafe, no bounds checking
func parseLegacy(_ buffer: UnsafeBufferPointer<UInt8>) -> Header {
Header(magic: buffer[0], version: buffer[1]) // Silent OOB crash
}
// ✅ New: safe, bounds-checked, same performance
func parseModern(_ span: Span<UInt8>) -> Header {
Header(magic: span[0], version: span[1]) // Traps on OOB
}
// Bridge: existing UnsafeBufferPointer → Span
let span = buffer.span // Wrap unsafe in safe span
parseModern(span)
OutputSpan — 安全な初期化
OutputSpan/OutputRawSpan は、中間割り当てを行わずに新しいコレクションを初期化するための UnsafeMutableBufferPointer の代替です。
// Binary serialization: write header bytes safely
@lifetime(&output)
func writeHeader(to output: inout OutputRawSpan) {
output.append(0x01) // version
output.append(0x00) // flags
output.append(UInt16(42)) // length (type-safe)
}
バイト配列、バイナリーシリアライゼーション、画像のピクセルデータの構築に使用します。Apple のオープンソースライブラリーである Swift Binary Parsing は、全面的に Span 型を使用して構築されています。
Span を使用すべきでない場合
- 所有権:Span は構造体やクラスに保存できません。所有するデータには Array を使用し、計算プロパティを介して
.spanにアクセスできるようにしてください - 戻り値:Span はエスケープできません。スコープ内で処理し、所有権のあるデータを返してください
- 長期間保持する参照:Span のライフタイムはソースに紐付けられています。データを現在のスコープより長く保持する必要がある場合は、Array を使用してください
コピーして使えるパターン
パターン 1:COW ラッパー
final class Storage<T> {
var value: T
init(_ value: T) { self.value = value }
}
struct COWWrapper<T> {
private var storage: Storage<T>
init(_ value: T) {
storage = Storage(value)
}
var value: T {
get { storage.value }
set {
if !isKnownUniquelyReferenced(&storage) {
storage = Storage(newValue)
} else {
storage.value = newValue
}
}
}
}
パターン 2:パフォーマンスが重要なループ
func processLargeArray(_ input: [Int]) -> [Int] {
var result = ContiguousArray<Int>()
result.reserveCapacity(input.count)
for element in input {
result.append(transform(element))
}
return Array(result)
}
パターン 3:インラインキャッシュ検索
private var cache: [Key: Value] = [:]
@inlinable
func getCached(_ key: Key) -> Value? {
return cache[key] // Inlined across modules
}
アンチパターン
| アンチパターン | 問題 | 修正方法 |
|---|---|---|
| 時期尚早な最適化 | プロファイリングデータがないのに、複雑な COW/ContiguousArray を使用する | まずはシンプルに始め、プロファイリングを行い、重要な部分を最適化します |
| あらゆる場所で weak を使用 | すべてのデリゲートに weak を付ける(アトミック処理のオーバーヘッドが発生します) | ライフタイムが保証される場合は unowned を使用します(パート 4 を参照) |
| あらゆる用途に Actor を使用 | 単純なカウンターに Actor 分離を使用する(呼び出しごとに約 100μs) | 単純な同期データには、ロックフリーのアトミック処理(ManagedAtomic)を使用します |
コードレビューのチェックリスト
メモリー管理
- 大きな構造体(64 バイト超)には、間接ストレージを使用するか、クラスを使用する
- COW 型では、変更前に
isKnownUniquelyReferencedを使用する - サイズがわかっている場合、コレクションで
reserveCapacityを使用する - 弱参照は必要な場合にのみ使用する(安全であれば unowned を優先する)
ジェネリクス
- 可能な場合、プロトコル型には
anyではなくsomeを使用する - ホットパスでは具象型または
@_specializeを使用する - ジェネリック制約はできる限り具体的にする
コレクション
- 純粋な Swift コードでは、
ArrayよりContiguousArrayを使用する - Dictionary のキーには、効率的な
hash(into:)実装を用意する - 短絡評価を行う操作には遅延評価を使用する
並行処理
- 同期処理に
asyncを使用しない - 可能な場合、Actor の呼び出しをまとめる
- Task の作成を最小限にする(TaskGroup を使用する)
- CPU 負荷の高い処理には
@concurrentを使用する(Swift 6.2)
最適化
- 最適化の前にプロファイリングデータを用意する
- インライン化は、小さく頻繁に呼び出される関数にのみ適用する
- メモリーレイアウトをキャッシュ局所性に合わせて最適化する(大きな構造体)
プレッシャーのかかる状況
シナリオ 1:「とにかく速くしてくれ。明日リリースだ」
プレッシャー:マネージャーがプロファイラーで「遅い」と表示されているのを見て、すぐに対応するよう求めます。
危険信号:
- ベースラインの測定値がない
- ホットスポットを示す Time Profiler のデータがない
- 目標を定めずに「すべてを速くする」よう求められる
所要時間の比較:
- 時期尚早な最適化:2 日間作業しても、測定可能な改善はなし
- プロファイリングに基づく最適化:2 時間のプロファイリング+実際のボトルネックの修正に 4 時間 = 40%高速化
プロフェッショナルに異議を伝える方法:
"I want to optimize effectively. Let me spend 30 minutes with Instruments
to find the actual bottleneck. This prevents wasting time on code that's
not the problem. I've seen this save days of work."
シナリオ 2:「どこでもアクターを使ってスレッドセーフにする」
プレッシャー:チームが Swift 6 を採用し、「すべてをアクターにすべきだ」と判断します。
注意すべき兆候:
- 単純な値型にアクターを使用する
- 同期処理のみにアクターを使用する
- タイトなループで非同期処理のオーバーヘッドが発生する
所要時間の比較:
- どこでもアクターを使用する:操作ごとに 100μs のオーバーヘッドが発生し、UI がぎくしゃくする
- 適切な分離を行う:オーバーヘッドは 10μs で、60fps の滑らかな動作を維持できる
プロフェッショナルな反論の仕方:
"Actors are great for isolation, but they add overhead. For this simple
counter, lock-free atomics are 10x faster. Let's use actors where we need
them—shared mutable state—and avoid them for pure value types."
シナリオ 3:「速度のためにすべてをインライン化する」
プレッシャー:誰かがインライン化は高速だと知り、すべてに @inlinable を付けます。
注意すべき兆候:
- 大きな関数に
@inlinableが付けられている - 内部実装の詳細が公開される
- バイナリサイズが 50% 増加する
所要時間の比較:
- すべてをインライン化する:コードが肥大化し、アプリの起動が遅くなる(3s → 5s)
- 選択的にインライン化する:起動が速く、実際のボトルネックを最適化できる
プロフェッショナルな反論の仕方:
"Inlining trades code size for speed. The compiler already inlines when
beneficial. Manual @inlinable should be for small, frequently called
functions. Let's profile and inline the 3 actual hotspots, not everything."
実例
例 1:画像処理パイプライン
問題:1000 枚の画像の処理に 30 秒かかります。
調査:
// Original code
func processImages(_ images: [UIImage]) -> [ProcessedImage] {
var results: [ProcessedImage] = []
for image in images {
results.append(expensiveProcess(image)) // Reallocations!
}
return results
}
解決策:
func processImages(_ images: [UIImage]) -> [ProcessedImage] {
var results = ContiguousArray<ProcessedImage>()
results.reserveCapacity(images.count) // Single allocation
for image in images {
results.append(expensiveProcess(image))
}
return Array(results)
}
結果:再割り当てをなくすことで、30s → 8s(73% 高速化)を実現しました。
例 2:ジェネリックの特殊化
問題:プロトコルベースのレンダリングが遅いです。
調査:
// Original - existential overhead
func render(shapes: [any Shape]) {
for shape in shapes {
shape.draw() // Dynamic dispatch
}
}
解決策:
// Specialized generic
func render<S: Shape>(shapes: [S]) {
for shape in shapes {
shape.draw() // Static dispatch after specialization
}
}
// Or use @_specialize
@_specialize(where S == Circle)
@_specialize(where S == Rectangle)
func render<S: Shape>(shapes: [S]) { }
結果:ウィットネステーブルのオーバーヘッドをなくすことで、100ms → 10ms(10 倍高速化)を実現しました。
リソース
WWDC:2025-312、2024-10217、2024-10170、2021-10216、2016-416
ドキュメント:/swift/inlinearray、/swift/span、/swift/outputspan
スキル:axiom-performance-profiling、axiom-swift-concurrency、axiom-swiftui-performance