Swiftは、indirectキーワードを通じて、値型列挙型に無限再帰を可能にします。これは、特定のケースがその関連値をヒープに確保された参照カウント付きボックスに格納することを強制します。indirectとしてマークされたケースでは、コンパイラはインラインペイロードのストレージを、ARCによって管理されるヒープに確保されたコンテナへのポインタに変換します。この間接参照により、列挙型は無限のサイズ拡張なしに再帰的に自分自身を参照でき、コンパイラはインラインで完全な値ではなくポインタを格納するだけで済みます。
しかし、この変換はパターンマッチングのパフォーマンスに大きな影響を与えます。indirectケースへの各アクセスはペイロードに到達するためにポインタ追跡を必要とするため、スタックに完全に格納された列挙型に比べてCPUキャッシュの局所性が低下します。さらに、ヒープの割り当ては、並行コンテキストでの同期オーバーヘッドを増大させる原子的な保持および解放操作を導入しますが、列挙型自体は言語レベルでの値セマンティクスを維持します。
indirect enum Expression { case literal(Int) case add(Expression, Expression) case multiply(Expression, Expression) } // パターンマッチングは逆参照を必要とします func evaluate(_ expr: Expression) -> Int { switch expr { case .literal(let value): return value case .add(let left, let right): return evaluate(left) + evaluate(right) case .multiply(let left, let right): return evaluate(left) * evaluate(right) } }
私たちは、深くネストされた論理式を処理する必要のある構成エンジンのドメイン特化言語パーサーを開発していました。初期の実装では、indirectアノテーションなしで表現ASTを表すために再帰的な列挙型を使用していたため、ネストの深さが数千レベルを超える構成ファイルを処理するとすぐにスタックオーバーフローが発生しました。
最初に考慮された解決策は、親子参照を持つクラスベースのツリー構造に完全に列挙型を放棄することでした。このアプローチは再帰関係のための自然なヒープ割り当てを提供しましたが、値セマンティクスを犠牲にするため、安全に解析された部分木を並行コンパイルスレッド間で共有することは困難でした。
私たちは二番目の解決策を選びました:列挙型内の再帰的なケースに特にindirectを適用することです。これは、再帰のために必要な場所でのみヒープ割り当てを強制し、値セマンティクスを維持しました。トレードオフは受け入れられました。なぜなら、イミュータビリティの保証と型安全性を維持する一方で、頻繁に変更される式ツリーに対してカスタムのコピーオンライト最適化を実装する必要があったからです。
その結果は、任意の深さのネストを処理できる安定したパーサーでした。後にプロファイリングした結果、indirectケースに対するパターンマッチングは、ポインタの間接参照およびARCトラフィックのために約20%多くのCPUサイクルを消費することが明らかになりました。この問題は、小さな固定深さの構造を一般的なケースのために非間接的な補助列挙型に平坦化することで軽減しました。
indirectはSwiftのコピーオンライト最適化とどのように相互作用しますか?
多くの候補者は、indirectケースが常に全体の再帰構造のディープコピーを引き起こすと仮定しています。実際には、Swiftは間接ペイロードを含むヒープボックスにコピーオンライトセマンティクスを適用します。indirectケースを持つ列挙型が新しい変数に代入されるとき、コンパイラは内容をコピーするのではなく、ヒープボックスの参照を保持します。ペイロードは、変更操作が発生して参照カウントが1を超えたときにのみコピーされます。この最適化は、大きな再帰構造に対するパフォーマンスにとって重要ですが、リファレンスカウント自体は原子的であるため、スレッドの安全性を考慮する必要があります。なぜなら、コピーオンライトロジックはスレッド間での同期を必要とするからです。
indirectを列挙型全体ではなく個々のケースに適用できますか?そのメモリレイアウトへの影響は何ですか?
候補者はしばしば、indirectが列挙型全体の宣言に適用されなければならないと信じています。しかし、Swiftは個々のケースをindirectとしてマークすることを許可しており、これがメモリレイアウトに大きな影響を与えます。特定のケースがindirectとしてマークされると、列挙型はタグ付きポインタ表現を使用し、間接ケースはヒープボックスへの単語サイズのポインタを占め、非間接ケースは列挙体のメモリフットプリント内にそのペイロードをインラインで格納します。この混合表現は、特定のケースのみが再帰を必要とする列挙型のメモリ使用を最適化します。しかし、これはインラインと間接ペイロードのために異なるアクセスコードパスを生成する必要があるため、パターンマッチングにおいて複雑さを導入します。また、列挙型の全体サイズは最大のインラインペイロードとタグビットで決定され、間接ケースのサイズではありません。
indirectを持つ再帰的列挙型がクロージャを含む場合、なぜ保持サイクルを生じる可能性があるのか、そしてこれが標準の値型の動作とどのように異なるのか?
これは、ARCの深い理解を明らかにする微妙なポイントです。通常、列挙型のような値型はアイデンティティを持たず、値レベルでの参照カウントがないため、保持サイクルを作成できません。しかし、ケースがindirectとしてマークされると、ペイロードはヒープに確保され、参照カウントされます。indirectケースの関連値に列挙型自体をキャプチャするクロージャが含まれ、そのクロージャが列挙型の関連値に保存される場合、ヒープボックスとクロージャの間に保持サイクルが発生します。これは、サイクルが列挙値自体ではなく、ヒープに確保されたボックス内に存在するため、クラスベースのサイクルとは異なります。サイクルを解消するためには、[weak self]や[unowned self]のようなキャプチャリストを使用する必要がありますが、列挙型は通常値型であるため、開発者はindirectがペイロードに対して参照セマンティクスを導入することをしばしば忘れ、クロージャを扱う際にクラスと同じ注意が必要になります。