Swiftがジェネリック関数をコンパイルする際、ジェネリックパラメータに置き換えられる具体的な型は、異なるモジュールやライブラリで定義され、異なるタイミングでコンパイルされる可能性があります。他の言語の早期のジェネリックスアプローチは、モノモーフィゼーション(各型のために別々のコードを生成すること)を必要とし、これがバイナリの膨張を引き起こし、ジェネリックの動的リンクを妨げます。Swiftは、パフォーマンスと別のコンパイルの柔軟性、ライブラリの変更に対するレジリエンスをバランスさせる解決策を必要としました。
問題: func process<T>(_ value: T)のようなジェネリック関数は、スコープを出る際にTをローカル変数にコピーしたり、移動させたり、破棄したりする必要があります。しかし、コンパイラはビルド時にTがトリビアルなInt(8バイト)であるのか、大きな構造体(4KB)であるのか、ヒープバッファを含む参照カウント型の構造体であるのかを知ることができません。この知識がなければ、関数はどれだけのスタックスペースを割り当てるべきか、メモリをどのようにアラインさせるべきか、Tが所有しているかもしれないヒープリソースのライフサイクルをどのように管理するかを知ることはできません。さらに、ArrayやDataのような**Copy-on-Write (COW)**型については、構造体の値をコピーする際には、バッファの深いコピーを行うのではなく、参照カウントをインクリメントするだけで済むようにする必要があります。
解決策: Swiftは**Value Witness Tables (VWT)**を採用しています。すべての型には、基本操作のための関数ポインタを含むVWT(またはレイアウト互換の型のために共通のVWTを共有)が存在します: size, alignment, stride, destroy, initializeWithCopy, assignWithCopy, initializeWithTake, assignWithTake。ジェネリックコードをコンパイルする際、LLVMはこれらのウィットネス関数への呼び出しを生成し、インライン命令の代わりにします。COW最適化のために、これらの型のinitializeWithCopyウィットネスは浅いコピーを行い(バッファ参照を保持)、実際のユニークネスチェックやバッファの複製は、型の自身のメソッドによる変更まで遅延されます。これにより、ジェネリックアルゴリズムはCOWのパフォーマンス特性を保ちながら、任意の値型を正しく処理することができます。
ユーザーがカスタムサンプルフォーマットを定義できる高性能オーディオ処理ライブラリを開発することを想像してみてください。あなたは、過剰なコピーなしでサンプルを効率的に保存および回転させるジェネリックRingBuffer<T>を実装する必要があります。バッファは、Float(4バイト)などの小さなトリビアル型や、COWセマンティクスを持つ16KBのヒープバッファをラップしたAudioPacket(構造体)などの大きな複雑な型を処理する必要があります。
考慮された解決策の1つは、ユーザーにclone()およびdispose()メソッドを持つClonableプロトコルに準拠することを要求することでした。このアプローチは完全な制御を提供しますが、すべての型に対してボイラープレートを書くことを強制し、Arrayのような標準ライブラリ型の直接使用を妨げ、dispose()を忘れるとメモリリークの危険性があります。また、トリビアル型に対するコンパイラ生成の最適化を活用できません。
別のアプローチでは、すべての操作にUnsafeMutablePointerとmemcpyを使用することが含まれました。Floatに対して高速ですが、これは参照カウントされる構造体やCOW型に対しては破綻し、ポインタ値を重複させることで、それらを保持せず、リングバッファが古いデータを上書きしたときに使用後解放のクラッシュやバッファの破損が発生することになります。これには手動のメモリ管理が必要で、エラーが発生しやすく、Swiftの安全性保証をバイパスします。
選ばれた解決策は、リングバッファをContiguousArray<T>でバックアップし、VWTを使用してすべての要素操作を内部的に行うというSwiftの組み込みのジェネリック機構を活用しました。回転ロジックには、withUnsafeMutableBufferPointerとmoveInitialize(from:count:)を組み合わせて使用し、VWTの移動ウィットネスを呼び出しました。これにより、コピーコンストラクタを呼び出さずに値の所有権を転送し、不必要な参照カウントのインクリメントを回避してCOWセマンティクスを保持しました。このアプローチは、エッジケースに対してVWTにフォールバックしつつ、ホットパスを特化させるコンパイラの能力を利用して、メモリ安全性を維持しながら最適に近いパフォーマンスを実現するために選ばれました。
その結果、カスタムプロトコルの要件やパブリックAPIにおける安全でないコードなしに、トリビアル型に対するO(1)のパフォーマンスを維持しつつ、大きなCOWオーディオパケットのためのゼロコピー回転が実現されました。
なぜ、ジェネリック関数内で大きな構造体をコピーすることが、専門的な非ジェネリックコンテキストでコピーするよりも遅く見えることがあるのか、たとえ両方が値セマンティクスを使用していても?
具体的な型が知られている専用のコンテキストでは、Swiftコンパイラはコピー操作をmemcpyまたはベクトル化されたSIMD命令としてインライン化できます。しかし、未専門化されたジェネリックコードでは、コピー操作はVWTのinitializeWithCopy関数ポインタを経由してディスパッチされます。この間接的な呼び出しによりインライン化が妨げられ、その結果、デッドストアの排除やベクトル化などの後続の最適化が制限されます。コンパイラは、コピーが副作用を持たないこと(例えば、参照に対する保持カウントなど)を証明できないため、保守的で遅いコードを生成せざるを得ません。この区別を理解することは、パフォーマンスクリティカルなジェネリックアルゴリズムにとって重要です。
Swiftは、ジェネリックイニシャライザがプロパティの割り当ての途中でエラーを投げた場合に、部分的に初期化された値をどのように破棄しますか?
ジェネリック構造体のイニシャライザがいくつかのプロパティを初期化した後、他のプロパティを初期化せずにエラーを投げた場合、Swiftはすでに初期化されている値が漏れないようにする必要があります。コンパイラは、逆初期化順序で各初期化済みプロパティのためにVWTのdestroyウィットネスを参照するエラークリーンアップパスを生成します。VWTは具体的な型の正確なレイアウトとクリーンアップ手順を知っているため、どの特定のプロパティが設定されたかを知らなくても、部分的に構築された値を正しく破棄できます。このメカニズムは、複雑な値型を伴う失敗シナリオにおいてもメモリの安全性を保証します。
Value Witness TablesとExistential Containersの関係は何であり、なぜ大きな値型がanyプロトコルにエレイすとヒープにアロケートされるのか?
Existential Container(any Protocolのボックス)は、通常3つのワード(64ビットシステムで24バイト)のインラインストレージを持っています。このインラインバッファよりも大きな値が存在論的型に消去されると、Swiftは値をヒープにアロケートし、コンテナにポインタを格納します。コンテナの型メタデータと一緒に、基礎となる型のVWTが保存されます。VWTはヒープボックスをアロケートするために必要なsizeとalignmentを提供し、存在論的スコープが終了するときにそれをクリーンアップするためのdestroyウィットネスを提供します。この分離により、存在論的コンテナは固定サイズを持ちながら、恣意的に大きな値型を収容できる一方で、大きな値に対してヒープアロケーションと間接参照のコストが発生します。