Swift utilizza un'ottimizzazione del compilatore nota come utilizzo di abitante extra (o imballaggio di bit di riserva) per eliminare l'overhead di archiviazione per il caso none di Optional. Per i tipi di riferimento (classi, chiusure, AnyObject), la rappresentazione del puntatore sottostante include un indirizzo nullo (0x0) che non è un riferimento a un oggetto valido; Swift riutilizza questo puntatore nullo per rappresentare Optional.none, mentre tutti i puntatori non nulli rappresentano Optional.some. Estendendo questo ai enum generali con più casi portatori di payload, il compilatore analizza i modelli di bit di tutti i tipi di valore associati per identificare valori comuni non utilizzati (bit di riserva). Se tutti i tipi di payload condividono almeno un numero sufficiente di bit di riserva per codificare il conteggio dei casi, l'enum memorizza il discriminante del caso all'interno di quei bit; in caso contrario, aggiunge un byte o una parola tag separati.
Durante l'architettura del grafo della scena per un motore di rendering 3D in tempo reale, il team doveva memorizzare riferimenti parent optional per 2 milioni di nodi della scena. Ogni nodo era un'istanza di classe, e la gerarchia richiedeva Optional<Node> per rappresentare i nodi radice (che non hanno genitore).
Soluzione A: Array booleano parallelo.
Il team ha considerato di mantenere un ContiguousArray<Bool> separato accanto a ContiguousArray<Node> per indicare la presenza del genitore.
Pro: Controllo esplicito, modello indipendente dal linguaggio.
Contro: La località della cache è distrutta accedendo a due regioni di memoria disgiunte; l'overhead di memoria aumentato di 2MB (1 byte per booleano, imbottito per allineamento); complessità di sincronizzazione durante la ristrutturazione dell'albero.
Soluzione B: Modello del nodo sentinella.
Utilizzando un'istanza singleton globale "nodo nullo" per rappresentare genitori assenti.
Pro: Memorizzazione di un singolo puntatore, nessun overhead opzionale.
Contro: Viola la sicurezza dei tipi; il compilatore non può prevenire operazioni accidentali sulla sentinella; richiede controlli difensivi in tutto il codice; introduce cicli di riferimento se la sentinella mantiene riferimenti ai nodi reali.
Soluzione C: Optional nativo di Swift.
Adottando direttamente Optional<Node> all'interno della struttura del nodo.
Pro: Sicurezza completa a tempo di compilazione, sintassi idiomatica di Swift, zero overhead di memoria poiché Optional utilizza la rappresentazione del puntatore nullo per none.
Contro: Richiede comprensione che questa ottimizzazione si applica specificamente ai tipi di riferimento; i tipi di valore come Int comporterebbero imbottitura.
Il team ha scelto Soluzione C. Poiché Node era una classe, il wrapper Optional non ha aggiunto byte alla dimensione dell'istanza. Il risultato è stato una riduzione della memoria di circa 16MB rispetto all'approccio booleano parallelo (eliminando sia la memorizzazione booleana che l'imbottitura di allineamento associata), guadagnando al contempo garanzie a tempo di compilazione che hanno eliminato un'intera classe di arresti anomali dovuti a dereferenziazione di null durante le successive ristrutturazioni.
Perché Optional<Int> occupa tipicamente più memoria di Int, mentre Optional<AnyObject> occupa lo stesso spazio di AnyObject?
Int è un intero in complemento a due a 64 bit che utilizza ogni possibile modello di bit per rappresentare il suo intervallo numerico (-2^63 a 2^63-1), non lasciando disponibili modelli di bit non validi (abitanti extra) per il discriminante Optional. Di conseguenza, il compilatore deve aggiungere un byte separato (o parola, a causa dell'allineamento) per memorizzare se l'opzione è some o none. Al contrario, AnyObject (e tutti i riferimenti a classi) sono puntatori in cui il modello di bit tutto zero (nullo) è garantito non valido come indirizzo oggetto; Optional rivendica questa rappresentazione nulla per il suo caso none, richiedendo zero spazio di archiviazione aggiuntivo.
Quante rappresentazioni distinte a livello di macchina esistono per "assenza" in Optional<Optional<T>> quando T è una classe, e perché questo è importante per l'uguaglianza?
Esistono due distinte rappresentazioni: il .none esterno (un puntatore nullo a livello esterno) e .some(.none) (un puntatore esterno valido che punta a un interno nullo). Poiché il Optional interno consuma già il valore del puntatore nullo per rappresentare la propria vuotezza, il Optional esterno non può distinguere il proprio none da un .some contenente un none interno utilizzando solo il valore del puntatore. Pertanto, lo strato esterno richiede un bit di tag separato, e i due "stati" concettuali di "nil" non sono uguali (Optional(Optional.none) != Optional.none). Questa distinzione è cruciale quando si annidano optionals restituiti da API generiche o dalla decodifica JSON dove chiavi mancanti producono nil esterni e valori null producono nil interni.
Quando si definisce un enum con più casi di payload, come case integer(Int), case boolean(Bool), cosa determina se il compilatore memorizza un byte tag separato o se incorpora il discriminante del caso all'interno del payload?
Il compilatore esegue un'analisi dei bit di riserva sui tipi di valore associati. Bool utilizza solo il bit meno significativo, lasciando 7 bit di riserva. Se tutti i payload dei casi fornivano bit di riserva sufficienti per identificare univocamente ciascun caso (ad esempio, più riferimenti a classi che condividono l'abitante extra nullo), l'enum potrebbe imballare l'indice del caso in quegli bit non utilizzati. Tuttavia, Int e Bool hanno modelli di bit di riserva disgiunti (Int non ne ha), costringendo il compilatore ad allocare un byte tag separato (o parola) per distinguere integer da boolean, aumentando la dimensione dell'enum rispetto alla dimensione massima del payload.