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)

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 を用いた最適なコード生成が可能になります。ランタイムアサートを用いる既存のオーバーロードも互換性のため引き続き利用できます。

今後の展望

本proposalのスコープ外ですが、いくつかの方向性が示されています。RegularBox のような型に対する条件付き適合の自動導出(derive)や、BitwiseCopyable への動的キャストのサポート、メモリ上でビット列の再配置によりムーブ可能な型を表す BitwiseMovable などが候補として挙げられています。また、BitwiseCopyable を将来的により基本的なプロトコルの合成(たとえば Bitwise & Copyable & DefaultDeinit)として再定義する余地も、ABI互換性を保ったまま残されています。いずれも見通しの段階で、実現を約束するものではありません。