Swift Digest
SE-0426 | Swift Evolution

BitwiseCopyable

Proposal
SE-0426
Authors
Kavon Farvardin, Guillaume Lessard, Nate Chandler, Tim Kientzle
Review Manager
Tony Allevato
Status
Implemented (Swift 6.0)

このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら

01 何が問題だったのか

Swiftはジェネリックなコードを、型ごとに特殊化されない形にコンパイルできます。この形では、コンパイルされた関数は値とその型情報を受け取り、コピーや破棄といった基本操作は「value witness関数」と呼ばれる関数テーブル経由の呼び出しとして実装されます。

柔軟ではありますが、このアプローチには少なくないオーバーヘッドが伴います。たとえば Int を大量に含むバッファをコピーする場合、値ごとに関数呼び出しが発生します。本来であれば memcpy のような一括メモリコピーで済むところを、値ごとの間接呼び出しで処理しなければなりません。

実際には、Int や浮動小数点数、ポインタ、Optional<Int> のように「ビット列をそのままコピーすれば複製でき、特別な破棄処理も要らない」型は数多く存在します。こうした型を bitwise-copyable(あるいはtrivial、POD)と呼びますが、これまでSwiftにはそれをジェネリック制約として表現する手段がありませんでした。そのため、標準ライブラリの UnsafeMutablePointer.initialize(to:count:) のようにbitwise-copyableな型なら一括コピーで高速化できるAPIも、静的にはその性質を利用できず、ランタイムでの確認やvalue witness経由の処理に頼らざるを得ませんでした。また、開発者自身が「この型がbitwise-copyableなときだけ高速パスを選ぶ」といった最適化を書くこともできませんでした。

さらに、ランタイムには _isPOD という「その型が今現在bitwise-copyableかどうか」を返す仕組みがありますが、これは一時的(transient)な性質にすぎません。library evolutionを有効にしたライブラリでは、public型のstored propertyが将来差し替えられることでbitwise-copyabilityが変わり得るため、ランタイムの状態だけでは「将来にわたってbitwise-copyableであり続ける」ことを保証できません。ジェネリック制約やAPIの契約として使える、恒久的(permanent)な表現が必要でした。

02 どのように解決されるのか

標準ライブラリに、memcpy によるムーブ・コピーが安全に行え、特別な破棄処理も不要な型を表すマーカープロトコル BitwiseCopyable が追加されます。

@_marker public protocol BitwiseCopyable {}

この制約をジェネリックパラメータに付けることで、コンパイラはvalue witness経由ではなく直接 memcpy 相当のメモリ操作を生成できます。開発者自身も、この制約を使って「bitwise-copyableな型向けの高速な一括コピー」などの特殊化APIを定義できるようになります。

適合のルール

Int や浮動小数点数、SIMD型、各種ポインタ型、UnmanagedOptional といった標準ライブラリの多くの基本型があらかじめ BitwiseCopyable に適合します。_Pointer / SIMD などのプロトコルにも BitwiseCopyable 制約が付きます。

加えて次の型もコンパイラが自動で BitwiseCopyable とみなします。

  • 要素がすべて BitwiseCopyable なタプル
  • unowned(unsafe) 参照(参照カウント操作が不要なため)
  • @convention(c) / @convention(thin) の関数型(参照カウントされるキャプチャコンテキストを持たないため)

開発者が定義するstructやenumでも、継承リストに BitwiseCopyable を書くことで明示的に適合を宣言できます。コンパイラは全フィールドが BitwiseCopyable であることをチェックし、そうでなければエラーになります。

public struct Coordinate: BitwiseCopyable {
  var x: Int
  var y: Int
}

public enum PositionUpdate: BitwiseCopyable {
  case begin(Coordinate)
  case move(x_change: Int, y_change: Int)
  case end
}

ジェネリック型に対しても同様で、無条件に適合できる場合もあれば、条件付き適合として書く必要がある場合もあります。

// Value が BitwiseCopyable であれば BittyBox も BitwiseCopyable
struct BittyBox<Value: BitwiseCopyable>: BitwiseCopyable {
  var first: Value
}

// Value に制約が無いので無条件には適合できない
struct RegularBox<Value> {
  var first: Value
}

// 条件付き適合を明示的に書けば OK
extension RegularBox: BitwiseCopyable where Value: BitwiseCopyable {}

他のモジュールで定義された型に対して、外部から BitwiseCopyable 適合を付与することはできません。

自動推論と抑制

モジュール内で定義されるnon-exportedなstruct / enumについては、全フィールドが BitwiseCopyable であればコンパイラが無条件の適合を自動推論します。ジェネリック型の場合は、フィールドが無条件に BitwiseCopyable であるときだけ推論され、条件付き適合は推論されないので必要なら手書きします。raw-value enumも推論の対象です。

一方、exported(public / package / @usableFromInline)な型については、library evolution下で将来フィールドが差し替えられる可能性があるため推論は行われません。将来にわたり BitwiseCopyable であり続けることを約束したい場合は、開発者が明示的に適合を宣言する必要があります。ただし @frozen が付いたpublic型については、フィールドがすべて BitwiseCopyable であることをコンパイラが確実に追跡できるため、推論されます。

@frozen
public struct Coordinate3 {
  public var x: Int
  public var y: Int
} // BitwiseCopyable への適合が自動推論される

自動推論を望まない場合は、継承リストに ~BitwiseCopyable を書いて抑制します。抑制は型宣言本体に書く必要があり、extensionには書けません。

struct Coordinate4: ~BitwiseCopyable { ... }

CやC++からインポートされる型にも同様の推論が適用されます。C / C++のenumは常に BitwiseCopyable に適合し、C / C++のstruct(C++の場合はnon-trivialでないもの)は全フィールドが BitwiseCopyable なら適合します。Swiftで表現できないフィールドを含むために推論が効かない型についても、__attribute__((__swift_attr__("BitwiseCopyable"))) を付けることで適合を上書き指定できます。

_isPOD との違い

ランタイムの _isPOD は「その型が今現在bitwise-copyableかどうか」を返す一時的(transient)な性質です。これに対して BitwiseCopyable への適合は、「その型は今も将来もbitwise-copyableであり続ける」という恒久的(permanent)な約束を表します。したがって、BitwiseCopyable に適合していれば _isPOD は常に真ですが、その逆は成り立ちません。

この違いから、BitwiseCopyable の適合はコンパイラが自動で付与してよい性質ではなく、library evolutionで動く型については開発者の明示的な宣言が必要になります。型のbitwise-copyabilityは進化の過程で失われたり再び獲得されたりし得ますが、一度付けた BitwiseCopyable 適合を外すことはソース互換・ABI互換を壊すため行えません。

マーカープロトコル由来の制限

BitwiseCopyable@_marker 属性付きで宣言されており、Sendable などと同じく制限付きのプロトコルです。

  • BitwiseCopyable をextendすることはできません(適合する全型の名前空間を汚染しないための制限です)。
  • 現時点では、BitwiseCopyable をランタイムで表現する仕組みが無いため、is / as? による動的キャストや、別プロトコルへの条件付き適合のジェネリック制約として使うことはできません。

これらの制限は将来の拡張余地を残すためのもので、必要になった段階でランタイム表現の追加などが検討される見込みです。

標準ライブラリAPIの追加

UnsafeRawPointer / UnsafeMutableRawPointerloadUnaligned(fromByteOffset:as:) と、UnsafeMutableRawPointerstoreBytes(of:toByteOffset:as:) に、T: BitwiseCopyable で制約された新しいオーバーロードが追加されます。

// UnsafeRawPointer / UnsafeMutableRawPointer
public func loadUnaligned<T: BitwiseCopyable>(
  fromByteOffset offset: Int = 0,
  as type: T.Type
) -> T

// UnsafeMutableRawPointer
public func storeBytes<T: BitwiseCopyable>(
  of value: T, toByteOffset offset: Int = 0, as type: T.Type
)

これまで T に対するランタイムアサート(デバッグビルドで _isPOD を確認する形)で安全性を担保していたのに対して、静的な制約によって安全性が保証され、memcpy を用いた最適なコード生成が可能になります。ランタイムアサートを用いる既存のオーバーロードも互換性のため引き続き利用できます。

03 今後の見通し

本proposalのスコープ外として、いくつかの方向性が示されています。いずれも将来の構想であり、実現を約束するものではありません。

条件付き適合の自動導出

RegularBox<Value> のように Value: BitwiseCopyable のときだけ BitwiseCopyable に適合できる型については、現状は手書きで条件付き適合を書く必要があります。将来的にはこのような条件付き適合をコンパイラが自動で導出できるようにする余地が残されています。

動的キャストのサポート

BitwiseCopyable はマーカープロトコルとしての制限から、現状では is / as? による動的キャストができません。そもそもlow-levelな性能向け機能なので動的キャストを許すべきかは議論の余地がありますが、もし将来サポートする場合のアプローチとして次の二つが想定されています。

  • 通常のプロトコルと同様に、型の BitwiseCopyable 適合をランタイムに記録してそれを問い合わせる方式。恒久的(permanent)な性質に基づく従来通りのキャスト動作になり、適合を抑制した型のキャストは失敗します。
  • 「現時点でbitwise-copyableな型」を一律に BitwiseCopyable とみなすduck typing方式。一時的(transient)な性質に基づくため、具体型として扱うときと動的キャスト経由で扱うときで挙動が変わったり、OSバージョンによって同じキャストが成功したり失敗したりし得ます。一方で、ランタイムに既存の IsNonPOD ビットがあるため完全にバックデプロイ可能という利点があります。

BitwiseMovable

ほとんどのSwiftの型は、メモリ上のビット列の再配置(直接的なメモリ操作)でムーブできます。この性質を表す BitwiseMovable プロトコルを BitwiseCopyable と同じ枠組みで導入する案も挙げられています。

合成プロトコルとしての再定義

ピッチ段階の議論では、BitwiseCopyableBitwise & Copyable & DefaultDeinit のような複数のプロトコルの合成として定義するアイデアも出ています。BitwiseCopyable@_marker 属性付きでABI上の影響が名前マングリングに限られるため、後から合成として再定義してもマングリングの形を揃えることでABI互換を保てる、という余地が残されています。