SwiftProgrammingSwift開発者

SwiftのOptional型は、参照型をラップする際に`none`ケースを追加のストレージなしで表現するためにどのようなメモリレイアウト最適化を利用しており、このメカニズムは複数のペイロードを持つケースを持つ列挙体にどのように拡張されますか?

Hintsage AIアシスタントで面接を突破

質問への回答

Swiftは、余分な住人の利用(またはスパービットパッキング)というコンパイラ最適化を使用して、Optionalnoneケースに対するストレージオーバーヘッドを排除します。参照型(クラスクロージャAnyObject)の場合、基礎となるポインタ表現には無効なオブジェクト参照ではないヌルアドレス(0x0)が含まれています。Swiftは、このヌルポインタをOptional.noneを表すために再利用し、すべての非ヌルポインタはOptional.someを表します。複数のペイロードを持つケースを持つ一般的な列挙体に拡張する際、コンパイラは関連する値の型のビットパターンを分析し、共通の未使用値(スパービット)を特定します。すべてのペイロードタイプがケース数をエンコードするのに十分なスパービットを共有している場合、列挙体はそれらのビット内にケース識別子を格納します。そうでない場合、それは別のタグバイトまたはワードを追加します。

実生活の状況

リアルタイム3Dレンダリングエンジンのシーングラフを設計する際、チームは200万のシーンノードに対してオプショナルな親参照を格納する必要がありました。各ノードはクラスインスタンスであり、階層はルートノードを表すためにOptional<Node>を必要としました(親がいない)。

解決策A: 平行ブール配列.
チームは、親の存在を示すためにContiguousArray<Bool>ContiguousArray<Node>とともに維持することを検討しました。
利点: 明示的な制御、言語に依存しないパターン。
欠点: 二つの非連続メモリ領域にアクセスすることでキャッシュローカリティが破壊され、メモリオーバーヘッドは2MB(1バイトあたりブール、アライメントにパディング)増加; ツリーの再構築時に同期の複雑さが増す。

解決策B: センチネルノードパターン.
存在しない親を表すために、グローバルシングルトン「ヌルノード」インスタンスを使用します。
利点: 単一のポインタストレージ、オプショナルオーバーヘッドなし。
欠点: 型安全性を侵害する; コンパイラはセンチネルに対する偶然の操作を防ぐことができない; コードベース全体で防御的チェックが必要; センチネルが実際のノードへの参照を保持している場合、参照サイクルを導入。

解決策C: ネイティブSwift Optional.
ノード構造体内でOptional<Node>を直接採用します。
利点: 完全なコンパイル時安全性、慣用的なSwift構文、Optionalnoneのためのヌルポインタ表現を使用するため、メモリオーバーヘッドなし。
欠点: この最適化が参照型に特有であることを理解する必要がある; Intのような値型はパディングが発生します。

チームは解決策Cを選びました。Nodeがクラスであったため、Optionalラッパーはインスタンスサイズにバイトを追加しませんでした。その結果、平行ブールアプローチに比べて約16MBのメモリ削減が得られ(ブールストレージと関連するアライメントパディングの両方を排除)、その後のリファクタリング中に発生するnull参照クラッシュの全クラスを排除するコンパイル時の保証を得ました。

候補者が見逃しがちなのは

なぜOptional<Int>は通常Intよりも多くのメモリを占有し、Optional<AnyObject>AnyObjectと同じスペースを占有するのか?

Intは64ビットの二の補数整数で、その数値範囲(-2^63から2^63-1まで)を表現するためにすべてのビットパターンを利用しているため、Optional識別子に利用可能な無効なビットパターン(余分な住人)が残っていません。したがって、コンパイラはオプショナルがsomenoneかを格納するために別のバイト(またはアライメントのためにワード)を追加する必要があります。一方、AnyObject(およびすべてのクラス参照)はポインタであり、すべてのゼロビットパターン(ヌル)はオブジェクトアドレスとして無効であることが保証されています; Optionalはこのヌル表現をnoneケースのために請求し、追加のストレージは必要ありません。

Optional<Optional<T>>における「欠如」のための異なるマシンレベルの表現はいくつ存在し、これは等価性にとってなぜ重要ですか?

二つの異なる表現が存在します:外側の.none(外側レベルでのヌルポインタ)および.some(.none)(内部のヌルを指す有効な外部ポインタ)。内部のOptionalはすでに自己的な空を表現するためにヌルポインタ値を消費しているため、外側のOptionalはポインタの値だけで自分自身のnone.someを持つ内側のnoneと区別することができません。したがって、外側のレイヤーは別のタグビットが必要であり、二つの概念的な「nil」状態は等しくありません(Optional(Optional.none) != Optional.none)。この区別は、ジェネリックAPIから返されるネストされたオプショナルや、欠落したキーが外部nilを生み出し、null値が内部nilを生み出すJSONデコードにおいて重要です。

複数のペイロードケースを持つ列挙体を定義する際、例えばcase integer(Int), case boolean(Bool)、コンパイラが別のタグバイトを格納するか、ペイロード内にケース識別子を埋め込むかを決定するのは何ですか?

コンパイラは関連する値型に対してスパービット分析を行います。Boolは最下位ビットのみを使用し、7ビットの余裕を残します。すべてのケースのペイロードが各ケースを一意に特定するのに十分な余裕ビットを提供する場合(例:ヌル余剰住人を共有するクラス参照)、列挙体はこれらの未使用ビットにケースインデックスをパックできます。しかし、IntBoolは異なる余裕ビットパターンを持ち(Intにはありません)、コンパイラはintegerbooleanを区別するために別のタグバイト(またはワード)を割り当てる必要があり、列挙体のサイズが最大ペイロードサイズを超えます。