Perché un int in OCaml ha solo 31 bit?


115

Non ho visto questa "caratteristica" da nessun'altra parte. So che il 32 ° bit viene utilizzato per la raccolta dei rifiuti. Ma perché è così solo per gli int e non per gli altri tipi di base?


10
Si noti che su sistemi operativi a 64 bit, un int in OCaml è di 63 bit, non 31. Ciò rimuove la maggior parte dei problemi pratici (come i limiti di dimensione dell'array) del bit del tag. E ovviamente c'è il tipo int32 se hai bisogno di un numero intero a 32 bit per un algoritmo standard.
Porculus

1
Anche nekoVM ( nekovm.org ) aveva int di 31 bit fino a poco tempo fa.
TheHippo

Risposte:


244

Questa è chiamata rappresentazione del puntatore con tag ed è un trucco di ottimizzazione piuttosto comune utilizzato in molti interpreti, VM e sistemi di runtime diversi per decenni. Praticamente ogni implementazione Lisp li utilizza, molte VM Smalltalk, molti interpreti Ruby e così via.

Di solito, in quelle lingue, si passano sempre i puntatori agli oggetti. Un oggetto stesso è costituito da un'intestazione dell'oggetto, che contiene i metadati dell'oggetto (come il tipo di un oggetto, le sue classi, forse restrizioni di controllo dell'accesso o annotazioni di sicurezza e così via), e quindi i dati dell'oggetto stesso. Quindi, un numero intero semplice sarebbe rappresentato come un puntatore più un oggetto composto da metadati e dall'intero effettivo. Anche con una rappresentazione molto compatta, è qualcosa come 6 Byte per un numero intero semplice.

Inoltre, non è possibile passare un oggetto intero di questo tipo alla CPU per eseguire operazioni aritmetiche di interi veloci. Se si desidera aggiungere due numeri interi, è in realtà hanno solo due puntatori, che indicano l'inizio delle intestazioni degli oggetti dei due interi oggetti che si desidera aggiungere. Quindi, devi prima eseguire l'aritmetica dei numeri interi sul primo puntatore per aggiungere l'offset all'oggetto in cui sono memorizzati i dati interi. Quindi devi dereferenziare quell'indirizzo. Ripeti lo stesso con il secondo numero intero. Ora hai due numeri interi che puoi effettivamente chiedere alla CPU di aggiungere. Ovviamente, ora devi costruire un nuovo oggetto intero per contenere il risultato.

Così, al fine di effettuare un'aggiunta intero, è effettivamente necessario eseguire tre aggiunte interi più due dererefences puntatore più una costruzione di un oggetto. E occupi quasi 20 byte.

Tuttavia, il trucco è che con i cosiddetti tipi di valore immutabili come gli interi, di solito non hai bisogno di tutti i metadati nell'intestazione dell'oggetto: puoi semplicemente lasciare fuori tutte queste cose e semplicemente sintetizzarle (che è VM-nerd- parlare per "fingere"), quando qualcuno si preoccupa di guardare. Un numero intero avrà sempre una classe Integer, non è necessario memorizzare separatamente tali informazioni. Se qualcuno usa la riflessione per capire la classe di un intero, rispondi semplicemente Integere nessuno saprà mai che non hai effettivamente memorizzato quell'informazione nell'intestazione dell'oggetto e che in realtà non c'è nemmeno un'intestazione dell'oggetto (o un oggetto).

Così, il trucco è quello di memorizzare il valore del l'oggetto all'interno del puntatore per l'oggetto, crollando in modo efficace i due in uno.

Esistono CPU che in realtà hanno spazio aggiuntivo all'interno di un puntatore (i cosiddetti bit di tag ) che consentono di memorizzare informazioni aggiuntive sul puntatore all'interno del puntatore stesso. Informazioni extra come "questo non è effettivamente un puntatore, questo è un numero intero". Gli esempi includono il Burroughs B5000, le varie Lisp Machine o l'AS / 400. Sfortunatamente, la maggior parte delle attuali CPU mainstream non ha questa caratteristica.

Tuttavia, c'è una via d'uscita: la maggior parte delle attuali CPU mainstream funzionano molto più lentamente quando gli indirizzi non sono allineati sui confini delle parole. Alcuni addirittura non supportano affatto l'accesso non allineato.

Ciò significa che in pratica tutti i puntatori saranno divisibili per 4, il che significa che finiranno sempre con due 0bit. Questo ci permette di distinguere tra puntatori reali (che terminano con 00) e puntatori che in realtà sono interi sotto mentite spoglie (quelli che terminano con 1). E ci lascia ancora con tutti i suggerimenti che finiscono per essere 10liberi di fare altre cose. Inoltre, la maggior parte dei sistemi operativi moderni riserva per se stessi gli indirizzi molto bassi, il che ci offre un'altra area con cui scherzare (puntatori che iniziano con, diciamo, 24 se 0finiscono con 00).

Quindi, puoi codificare un numero intero a 31 bit in un puntatore, semplicemente spostandolo di 1 bit a sinistra e aggiungendolo 1. E puoi eseguire aritmetiche di interi molto veloci con quelli, semplicemente spostandoli in modo appropriato (a volte nemmeno quello è necessario).

Cosa facciamo con quegli altri spazi di indirizzi? Ebbene, esempi tipici includono codificante floats nell'altro grande spazio indirizzo che il numero di oggetti speciali come true, false, nil, i 127 caratteri ASCII, alcuni brevi stringhe comunemente usati, la lista vuota, l'oggetto vuoto, la matrice vuota e così via vicino alla 0indirizzo.

Ad esempio, negli interpreti MRI, YARV e Rubinius Ruby, gli interi sono codificati nel modo descritto sopra, falsesono codificati come indirizzo 0(che è anche la rappresentazione di falsein C), truecome indirizzo 2(che è proprio così la rappresentazione in C di truespostata di un bit) e nilcome 4.


5
Ci sono persone che dicono che questa risposta è imprecisa . Non ho idea se questo è il caso o se stanno facendo il pelo nell'uovo. Ho solo pensato di indicarlo nel caso contenesse qualche verità.
surfmuggle

5
@threeFourOneSixOneThree Questa risposta non è completamente accurata per OCaml perché, in OCaml, la parte "sintetizzala" di questa risposta non ha mai luogo. OCaml non è un linguaggio orientato agli oggetti come Smalltalk o Java. Non c'è mai alcun motivo per recuperare la tabella dei metodi di un OCaml int.
Pascal Cuoq

Il motore V8 di Chrome utilizza anche un puntatore con tag e memorizza un numero intero a 31 bit chiamato smi (Small Integer) come ottimizzazione \
phuclv

@phuclv: Questo non è sorprendente, ovviamente. Proprio come HotSpot JVM, V8 si basa sulla VM Animorphic Smalltalk, che a sua volta si basa sulla Self VM. E V8 è stato sviluppato da (alcune delle) stesse persone che hanno sviluppato HotSpot JVM, Animorphic Smalltalk VM e Self VM. Lars Bak, in particolare, ha lavorato su tutti questi, oltre alla sua VM Smalltalk chiamata OOVM. Quindi, non sorprende affatto che V8 utilizzi trucchi ben noti dal mondo Smalltalk, poiché è stato creato da Smalltalkers basato sulla tecnologia Smalltalk.
Jörg W Mittag

28

Vedere la sezione "rappresentazione di numeri interi, bit di tag, valori allocati in heap" di https://ocaml.org/learn/tutorials/performance_and_profiling.html per una buona descrizione.

La risposta breve è che è per le prestazioni. Quando si passa un argomento a una funzione, viene passato come numero intero o come puntatore. A livello di linguaggio a livello di macchina non c'è modo di sapere se un registro contiene un numero intero o un puntatore, è solo un valore a 32 o 64 bit. Quindi il runtime di OCaml controlla il bit del tag per determinare se ciò che ha ricevuto era un numero intero o un puntatore. Se il bit del tag è impostato, il valore è un numero intero e viene passato all'overload corretto. Altrimenti è un puntatore e viene cercato il tipo.

Perché solo i numeri interi hanno questo tag? Perché tutto il resto viene passato come un puntatore. Ciò che viene passato è un numero intero o un puntatore a un altro tipo di dati. Con un solo bit di tag, possono esserci solo due casi.


1
"La risposta breve è che è per le prestazioni". In particolare le prestazioni di Coq. Le prestazioni di quasi tutto il resto risentono di questa decisione progettuale.
JD

17

Non è esattamente "utilizzato per la raccolta dei rifiuti". Viene utilizzato per distinguere internamente tra un puntatore e un numero intero unboxed.


2
E il corollario di ciò è che è così per almeno un altro tipo, vale a dire i puntatori. Se i float non sono anche 31 bit, presumo sia perché sono memorizzati come oggetti nell'heap e indicati con puntatori. Immagino che ci sia una forma compatta per array di questi, però.
Tom Anderson,

2
Queste informazioni sono esattamente ciò di cui il GC ha bisogno per navigare nel grafico del puntatore.
Tobu

"Viene utilizzato per distinguere internamente tra un puntatore e un numero intero unboxed". Qualcos'altro lo usa per quello diverso dal GC?
JD

13

Devo aggiungere questo collegamento per aiutare l'OP a capire di più Un tipo a virgola mobile a 63 bit per OCaml a 64 bit

Sebbene il titolo dell'articolo sembri riguardante float, in realtà si parla diextra 1 bit

Il runtime OCaml consente il polimorfismo attraverso la rappresentazione uniforme dei tipi. Ogni valore di OCaml è rappresentato come una singola parola, in modo che sia possibile avere una singola implementazione per, diciamo, "lista di cose", con funzioni per accedere (es. List.length) e costruire (es. List.map) queste liste che funzionano allo stesso modo sia che si tratti di elenchi di interi, di float o di elenchi di insiemi di interi.

Tutto ciò che non rientra in una parola viene allocato in un blocco nell'heap. La parola che rappresenta questi dati è quindi un puntatore al blocco. Poiché l'heap contiene solo blocchi di parole, tutti questi puntatori sono allineati: i loro pochi bit meno significativi sono sempre non impostati.

I costruttori senza argomenti (come questo: type fruit = Apple | Orange | Banana) e gli interi non rappresentano così tante informazioni da dover essere allocate nell'heap. La loro rappresentazione è unboxed. I dati sono direttamente all'interno della parola che altrimenti sarebbe stata un puntatore. Quindi, mentre un elenco di elenchi è in realtà un elenco di puntatori, un elenco di int contiene gli interi con un riferimento indiretto in meno. Le funzioni che accedono e costruiscono liste non si notano perché int e puntatori hanno la stessa dimensione.

Tuttavia, il Garbage Collector deve essere in grado di riconoscere i puntatori da interi. Un puntatore punta a un blocco ben formato nell'heap che è per definizione attivo (poiché viene visitato dal GC) e dovrebbe essere contrassegnato così. Un numero intero può avere qualsiasi valore e, se non vengono prese precauzioni, potrebbe apparire accidentalmente come un puntatore. Ciò potrebbe far sembrare vivi i blocchi morti, ma molto peggio, farebbe anche sì che il GC cambi bit in ciò che pensa sia l'intestazione di un blocco attivo, quando in realtà sta seguendo un numero intero che sembra un puntatore e incasina l'utente dati.

Questo è il motivo per cui gli interi unboxed forniscono 31 bit (per OCaml a 32 bit) o ​​63 bit (per OCaml a 64 bit) al programmatore OCaml. Nella rappresentazione, dietro le quinte, viene sempre impostato il bit meno significativo di una parola contenente un intero, per distinguerla da un puntatore. Gli interi a 31 o 63 bit sono piuttosto insoliti, quindi chiunque usi OCaml lo sa. Ciò che gli utenti di OCaml di solito non sanno è perché non esiste un tipo float unboxed a 63 bit per OCaml a 64 bit.


3

Perché un int in OCaml ha solo 31 bit?

Fondamentalmente, per ottenere le migliori prestazioni possibili con il prover del teorema di Coq in cui l'operazione dominante è il pattern matching ei tipi di dati dominanti sono tipi varianti. La migliore rappresentazione dei dati è risultata essere una rappresentazione uniforme che utilizza i tag per distinguere i puntatori dai dati unboxed.

Ma perché è così solo per gli int e non per gli altri tipi di base?

Non solo int. Altri tipi come chare enumerazioni utilizzano la stessa rappresentazione con tag.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.