Swift 5.9以前、開発者は異種の型コレクションで動作するジェネリックコードを書く際に、重要な表現の制限に直面していました。異なる型で保存された引数の可変数を必要とする関数は、型安全性を犠牲にし、ヒープ割り当てのオーバーヘッドを伴うAnyや存在論的コンテナ(any P)を使用せざるを得ませんでした。Parameter Packs(SE-0393、SE-0398、SE-0399)の導入により、Swiftに可変長ジェネリックがもたらされ、C++のテンプレートメタプログラミングやRustの可変長トレイトを必要としなかったパターンを表現できるようになりました。この進化は、ジェネリックプログラミングの根本的なギャップに対処し、手動でのオーバーロード生成なしに異種データに対する型安全なゼロコストの抽象化を可能にしました。
コアの課題は、任意の数のジェネリック引数(それぞれ異なる型の可能性がある)を受け入れつつ、呼び出しチェーンを通じて静的型情報を保持できるメカニズムを実装することでした。[Any]を使用した事前のパラメータパックソリューションは、実行時のキャスティングが必要であり、型関係を保持せず、インライン化や特殊化ディスパッチのようなコンパイラの最適化を妨げました。対照的に、1からNまでのアリティのオーバーロードを手動で生成すること(例:<T1>, <T1, T2>, <T1, T2, T3>)は、バイナリの膨張を引き起こし、引数のカウントに恣意的な制限を課しました。解決策は、コンパイル時にパックを反復処理し、コンパイラが各呼び出しサイトの型シグネチャに特有のモノモーフィズドコードを生成できるようにする必要がありました。
Swiftは、パック拡張を通じてパラメーターパックを実装し、パターンrepeat each Tをコード生成のためのコンパイル時テンプレートとして扱います。関数が型パラメーターパック<each T>を宣言し、値パックrepeat each Tを受け入れると、コンパイラは呼び出しサイトでモノモーフィゼーションを行い、ジェネリックボディをパック内の各要素に対する具体的なコードに展開します。これは、各要素がその独自の型のアイデンティティを保持するため、同種の可変引数(例:Int...)とは異なります。repeatキーワードは、SIL(Swift Intermediate Language)生成フェーズにおいて、後続の式が各パック要素に対して複製されるべきであることを示し、型をそれに応じて置き換えます。この変換は、値型が具体的なレイアウトでスタックに留まるためボクシングを排除し、関数呼び出しが存在論的コンテナオーバーヘッドなしで静的にディスパッチされることを保証します。
// 異種パラメーターパックを受け入れる関数 func describeValues<each T>(_ values: repeat each T) { // コンパイラはこのループをコンパイル時に展開します repeat print("型: \(type(of: each values)), 値: \(each values)") } // 使用は、以下に等しい特殊化コードを生成します: // describeValues(Int, String, Double) describeValues(42, "Swift", 3.14)
私たちのチームはiOS向けの高性能データパイプラインフレームワークを設計しており、ユーザーは異種の変換ステップ(例:DecodeJSON<T>、Validate<U>、Map<V>)を単一の実行グラフに連鎖させる必要がありました。APIは、これらのステップの数に制限を設けることなく、各ステップが異なる入力および出力型を保持しつつ、データフローのコンパイル時の知識を維持するpipeline関数を必要としました。
最初に1から6のジェネリック引数に対してオーバーロードを実装しました(例:func pipeline<T1, T2>(_: T1, _: T2))。これにより静的型が保持され、LLVMは全体のチェーンをインライン化できました。しかし、このアプローチは冗長でメンテナンスが困難で、ほぼ同じコードの何百行も必要でした。ユーザーを6ステップに制限し、追加のアリティを増やすごとにバイナリサイズが指数的に増加しました。8ステップをサポートする必要が生じた際のリファクタリング作業は膨大でした。
次に、関連型を持つAnyPipelineStepプロトコルを定義し、パラメータとして[any AnyPipelineStep]を使用することを試みました。これにより、無制限のステップがサポートされましたが、すべての値型(デコードされたデータを保持する構造体)がヒープ割り当てされた存在論的コンテナに強制的に格納されました。パフォーマンスプロファイリングは、CPU時間の30%がこれらのボックスでのswift_retainとswift_releaseオペレーションに費やされていることを示しました。さらに、コンパイラは関連型が消去されるため、ステップの境界を越えて最適化を行うことができず、各接続点で動的キャスティングが必要になりました。
Swift 5.9を用いて、APIをfunc pipeline<each Step: PipelineStep>(steps: repeat each Step)にリファクタリングしました。これにより、コンパイラはコードベースで見つかったすべての異なるパイプライン構成に対してユニークな特殊化を生成できるようになりました。各ステップはその具体的な型を保持し、攻撃的なインライン化と一時データ構造のスタック割り当てが可能になりました。repeatキーワードにより、コンパイル時に隣接ステップ間の型の互換性を確認するためにパックを反復処理しました。
アリティの制限を排除しながらパフォーマンスを犠牲にしないため、パラメーターパックを採用しました。存在論的なものとは異なり、パックはSwiftの最適化ツールに対してジェネリックシグネチャを保持し、ゼロコストの抽象化を実現しました。リファクタリングにより、オーバーロードアプローチと比較してフレームワークのバイナリサイズは35%削減され、存在論的アプローチと比較してスループットは4倍向上しました。開発者は各ステップの特定の入出力型に対して、自動補完サポートを完全に利用しつつ、任意の長さのパイプラインを構成できるようになり、データ不一致を統合テストの前にビルド時にキャッチできるようになりました。
候補者はしばしば、パック制約が単一のジェネリック制約のように動作する前提を持ちますが、Swiftはwhere句において明示的なrepeatパターンを必要とします。各パック要素の型TをContainerに適合させるとき、異なるItem関連型を持つ場合、構文はfunc process<each T: Container>(_ items: repeat each T) where repeat each T.Item: Equatableとなります。コンパイラは構造的制約解決を行い、where句要素をパック全体にわたって展開します。一般的な失敗モードは、パック全体に対して単一の関連型制約を使おうとすることですが、これは各T.Itemが異なる型であるため失敗します。計算のエラーをデバッグするためには、パック制約が各要素の要件の接続を生成することを理解することが重要です。
開発者はしばしば、パラメーターパックがすべてのコンテキストでゼロコストの抽象化を保証すると信じますが、ABI境界を越えたり、不透明な結果型を使用することでボクシングを強制される場合があります。特に、パラメーターパックがエスケープクロージャにキャプチャされ、異なるレジリエンスドメインの関数に渡される場合(例:公開ライブラリインターフェース)、Swiftは静的特殊化の代わりにウィットネステーブルを使用したランタイムジェネリックインスタンス化を発生させることがあります。同様に、パックイテレーション内からsome Collectionを返すことも、具体的な返り値型が各パック要素で異なるため、コンパイラが存在論的コンテナを用いるように強制されます。これは、存在論的インラインバッファのヒープ割り当て(3ワード)を導入し、プロトコルのウィットネステーブルを通じた間接参照を追加することでメモリレイアウトに影響します。呼び出しサイトにおいてパック全体の静的な可視性を要求することが、パフォーマンス維持にとって重要です。
この制限は、候補者がstruct Storage<each T> { repeat var item: each T }が各パック要素に対する異なる格納プロパティを宣言すると期待するため、混乱を招きます。Swiftは、格納プロパティがメモリ管理に必要な固定オフセットとストライドを必要とするため、これを禁止しています。可変数のプロパティは、ジェネリック型に対するABIの安定性要件を違反して可変サイズの構造体を生成します—値ウィットネステーブルは、インスタンスのコピー、移動、破壊のための静的レイアウトを期待します。(repeat each T)への集約を強制することにより、コンパイラはパックをその要素の直交積から導出されたレイアウトを持つ単一の複合値として扱います。これにより、Storageの各特殊化に決定的なバイナリレイアウトが保証され、ランタイムは動的なメタデータのルックアップなしに適切な値ウィットネス関数を選択できます。関数の引数としての一時的なパラメーターパックと、格納(structフィールド)としての持続的なストレージの違いを理解することで、パックが持続的なストレージのために「凍結」されなければならない理由が明確になります。