Swift nutzt eine Compiler-Optimierung, die als Nutzung zusätzlicher Bewohner (oder Spare Bit Packing) bekannt ist, um den Speicheraufwand für den Fall none von Optional zu eliminieren. Bei Referenztypen (Klassen, Closures, AnyObject) umfasst die zugrunde liegende Zeiger-Darstellung eine Null-Adresse (0x0), die keine gültige Objekt-Referenz darstellt; Swift verwendet diesen Nullzeiger, um Optional.none darzustellen, während alle nicht-null Zeiger Optional.some repräsentieren. Bei der Erweiterung auf allgemeine Enums mit mehreren payload-tragenden Fällen analysiert der Compiler die Bitmuster aller zugehörigen Werttypen, um gemeinsame ungenutzte Werte (Spare Bits) zu identifizieren. Wenn alle Payload-Typen mindestens genügend Spare Bits teilen, um die Fallanzahl zu kodieren, speichert das Enum den Fall-Diskriminator innerhalb dieser Bits; andernfalls wird ein separates Tag-Byte oder -Wort angefügt.
Bei der Architektur des Szenengitters für eine Echtzeit-3D-Rendering-Engine musste das Team optionale Parent-Referenzen für 2 Millionen Szenen-Knoten speichern. Jeder Knoten war eine Klasse-Instanz, und die Hierarchie erforderte Optional<Node>, um die Wurzelknoten darzustellen (die keinen Elternteil haben).
Lösung A: Paralleles Boolesches Array.
Das Team erwog die Verwendung eines separaten ContiguousArray<Bool> neben ContiguousArray<Node>, um die Anwesenheit des Elternteils anzuzeigen.
Vorteile: Explizite Kontrolle, sprachunabhängiges Muster.
Nachteile: Cache-Lokalität wird durch den Zugriff auf zwei getrennte Speicherregionen zerstört; Speicheraufwand erhöhte sich um 2MB (1 Byte pro Bool, gepolstert auf Ausrichtung); Synchronisationskomplexität bei der Umstrukturierung des Baumes.
Lösung B: Sentinel-Knotenmuster.
Verwendung einer globalen Singleton-"Null-Knoten"-Instanz, um abwesende Eltern darzustellen.
Vorteile: Einfache Zeigerspeicherung, kein optionaler Overhead.
Nachteile: Verletzt die Typensicherheit; der Compiler kann nicht versehentliche Operationen auf dem Sentinel verhindern; erfordert defensiven Prüfungen im gesamten Code; führt zu Referenzzyklen, wenn der Sentinel Referenzen auf echte Knoten hält.
Lösung C: Native Swift Optional.
Direkte Annahme von Optional<Node> innerhalb der Knotenstruktur.
Vorteile: Vollständige Sicherheit zur Compile-Zeit, idiomatische Swift-Syntax, null Speicheraufwand, da der Optional die Nullzeiger-Darstellung für none verwendet.
Nachteile: Erfordert das Verständnis, dass diese Optimierung speziell für Referenztypen gilt; Werttypen wie Int würden Padding verursachen.
Das Team wählte Lösung C. Da Node eine Klasse war, fügte der Optional-Wrapper keine Bytes zur Instanzgröße hinzu. Das Ergebnis war eine Einsparung von etwa 16 MB im Vergleich zum parallelen booleschen Ansatz (Eliminierung sowohl der booleschen Speicherung als auch des damit verbundenen Ausrichtungs-Paddings), während compile-time-Garantien gewonnen wurden, die eine gesamte Klasse von Null-Dereferenz-Abstürzen bei nachfolgenden Refaktorisierungen beseitigten.
Warum belegt Optional<Int> normalerweise mehr Speicher als Int, während Optional<AnyObject> den gleichen Platz wie AnyObject belegt?
Int ist eine 64-Bit-Zweier-komplementierte Ganzzahl, die jedes mögliche Bitmuster zur Darstellung ihres numerischen Bereichs (-2^63 bis 2^63-1) nutzt, sodass keine ungültigen Bitmuster (zusätzliche Bewohner) für den Optional-Diskriminanten verfügbar sind. Folglich muss der Compiler ein separates Byte (oder Wort, aufgrund der Ausrichtung) anhängen, um anzugeben, ob das Optional some oder none ist. Im Gegensatz dazu sind AnyObject (und alle Klassenreferenzen) Zeiger, bei denen das Null-Bitmuster (null) als Objektadresse garantiert ungültig ist; Optional beansprucht diese Nulldarstellung für seinen none-Fall, wodurch null zusätzlichen Speicher erforderlich ist.
Wie viele unterschiedliche Maschinenebenen-Darstellungen gibt es für "Abwesenheit" in Optional<Optional<T>>, wenn T eine Klasse ist, und warum ist das wichtig für die Gleichheit?
Es gibt zwei unterschiedliche Darstellungen: das äußere .none (ein Nullzeiger auf der äußeren Ebene) und .some(.none) (ein gültiger äußerer Zeiger, der auf ein inneres Null zeigt). Da der innere Optional bereits den Nullzeigerwert verwendet, um seine eigene Leere darzustellen, kann der äußere Optional seinen eigenen none nicht von einem .some, das ein inneres none enthält, nur anhand des Zeigerwerts unterscheiden. Daher benötigt die äußere Schicht ein separates Tag-Bit, und die beiden konzeptionellen "nil"-Zustände sind nicht gleich (Optional(Optional.none) != Optional.none). Diese Unterscheidung ist entscheidend, wenn optionale Typen von generischen APIs oder JSON-Dekodierungen zurückgegeben werden, wo fehlende Schlüssel äußere Nullen und Nullwerte innere Nullen erzeugen.
Wie bestimmt der Compiler, ob er ein separates Tag-Byte speichert oder den Fall-Diskriminator innerhalb der Payload einbettet, wenn er ein Enum mit mehreren Payload-Fällen definiert, wie case integer(Int), case boolean(Bool)?
Der Compiler führt eine Analyse der Spare Bits für die zugehörigen Werttypen durch. Bool verwendet nur das am wenigsten signifikante Bit und lässt 7 Bits übrig. Wenn alle Payloads der Fälle ausreichend Spare Bits bereitstellen, um jeden Fall eindeutig zu identifizieren (z. B. mehrere Klassenreferenzen, die den Null-Extra-Bewohner teilen), könnte das Enum den Fallindex in diesen ungenutzten Bits packen. Bei Int und Bool bestehen jedoch disjunkte Spare Bitmuster (Int hat keine), was den Compiler zwingt, ein separates Tag-Byte (oder Wort) zuzuweisen, um integer von boolean zu unterscheiden, wodurch die Größe des Enums über die maximale Payload-Größe hinaus erhöht wird.