Si suppone che il compilatore produca assemblatore (e infine codice macchina) per alcune macchine, e generalmente C ++ cerca di essere simpatico con quella macchina.
Essere solidali con la macchina sottostante significa approssimativamente: semplificare la scrittura di codice C ++ che mapperà in modo efficiente sulle operazioni che la macchina può eseguire rapidamente. Pertanto, vogliamo fornire accesso ai tipi di dati e alle operazioni che sono veloci e "naturali" sulla nostra piattaforma hardware.
Concretamente, si consideri una specifica architettura della macchina. Prendiamo l'attuale famiglia Intel x86.
Il Manuale dello sviluppatore del software per architetture Intel® 64 e IA-32 vol 1 ( link ), sezione 3.4.1 dice:
I registri di uso generale a 32 bit EAX, EBX, ECX, EDX, ESI, EDI, EBP ed ESP sono forniti per contenere i seguenti elementi:
• Operandi per operazioni logiche e aritmetiche
• Operandi per i calcoli degli indirizzi
• Puntatori di memoria
Quindi, vogliamo che il compilatore utilizzi questi registri EAX, EBX ecc. Quando compila l'aritmetica intera C ++ semplice. Ciò significa che quando dichiaro un int, dovrebbe essere qualcosa di compatibile con questi registri, in modo da poterli utilizzare in modo efficiente.
I registri hanno sempre le stesse dimensioni (qui, 32 bit), quindi il mio int variabili saranno sempre 32 bit. Userò lo stesso layout (little-endian) in modo da non dover fare una conversione ogni volta che carico un valore di variabile in un registro o immagazzino un registro in una variabile.
Usando godbolt possiamo vedere esattamente cosa fa il compilatore per qualche codice banale:
int square(int num) {
return num * num;
}
compila (con GCC 8.1 e -fomit-frame-pointer -O3per semplicità) per:
square(int):
imul edi, edi
mov eax, edi
ret
questo significa:
- il
int numparametro è stato passato nel registro EDI, il che significa che è esattamente la dimensione e il layout che Intel si aspetta da un registro nativo. La funzione non deve convertire nulla
- la moltiplicazione è una singola istruzione (
imul), che è molto veloce
- restituire il risultato è semplicemente una questione di copiarlo in un altro registro (il chiamante si aspetta che il risultato sia inserito in EAX)
Modifica: possiamo aggiungere un confronto rilevante per mostrare la differenza utilizzando un layout non nativo. Il caso più semplice è la memorizzazione di valori in qualcosa di diverso dalla larghezza nativa.
Usando di nuovo godbolt , possiamo confrontare una semplice moltiplicazione nativa
unsigned mult (unsigned x, unsigned y)
{
return x*y;
}
mult(unsigned int, unsigned int):
mov eax, edi
imul eax, esi
ret
con il codice equivalente per una larghezza non standard
struct pair {
unsigned x : 31;
unsigned y : 31;
};
unsigned mult (pair p)
{
return p.x*p.y;
}
mult(pair):
mov eax, edi
shr rdi, 32
and eax, 2147483647
and edi, 2147483647
imul eax, edi
ret
Tutte le istruzioni aggiuntive riguardano la conversione del formato di input (due numeri interi senza segno a 31 bit) nel formato che il processore può gestire in modo nativo. Se volessimo salvare il risultato in un valore a 31 bit, ci sarebbero un'altra o due istruzioni per farlo.
Questa ulteriore complessità significa che ti preoccuperesti di questo solo quando il risparmio di spazio è molto importante. In questo caso stiamo salvando solo due bit rispetto all'utilizzo del nativo unsignedo del uint32_ttipo, che avrebbe generato un codice molto più semplice.
Una nota sulle dimensioni dinamiche:
L'esempio sopra è ancora valori a larghezza fissa anziché a larghezza variabile, ma la larghezza (e l'allineamento) non corrispondono più ai registri nativi.
La piattaforma x86 ha diverse dimensioni native, tra cui 8-bit e 16-bit oltre ai principali 32-bit (sto passando alla modalità 64-bit e varie altre cose per semplicità).
Questi tipi (char, int8_t, uint8_t, int16_t ecc) sono anche supportati direttamente dall'architettura, in parte per la compatibilità con le versioni precedenti di 8086/286/386 / ecc. set di istruzioni ecc.
È certamente il caso di scegliere la dimensione fissa naturale più piccola sufficiente, può essere una buona pratica: sono ancora veloci, vengono caricate e memorizzate singole istruzioni, si ottiene ancora un'aritmetica nativa a piena velocità e si può persino migliorare le prestazioni riduzione dei mancati cache.
Questo è molto diverso dalla codifica a lunghezza variabile: ho lavorato con alcuni di questi e sono orribili. Ogni carico diventa un ciclo anziché una singola istruzione. Ogni negozio è anche un ciclo. Ogni struttura ha una lunghezza variabile, quindi non è possibile utilizzare le matrici in modo naturale.
Un'ulteriore nota sull'efficienza
Nei commenti successivi, hai usato la parola "efficiente", per quanto ne so per quanto riguarda le dimensioni di archiviazione. A volte scegliamo di ridurre al minimo le dimensioni di archiviazione: può essere importante quando salviamo un numero molto elevato di valori nei file o li inviamo in rete. Il compromesso è che dobbiamo caricare quei valori nei registri per fare qualsiasi cosa con loro, ed eseguire la conversione non è gratuito.
Quando parliamo di efficienza, dobbiamo sapere cosa stiamo ottimizzando e quali sono i compromessi. L'uso di tipi di archiviazione non nativi è un modo per scambiare la velocità di elaborazione con lo spazio e talvolta ha senso. Utilizzando una memoria a lunghezza variabile (almeno per i tipi aritmetici), viene scambiata una maggiore velocità di elaborazione (e complessità del codice e tempo di sviluppo) per un ulteriore risparmio di spazio spesso minimo.
La penalità di velocità che paghi per questo significa che vale la pena solo quando devi minimizzare assolutamente la larghezza di banda o l'archiviazione a lungo termine, e in quei casi è solitamente più facile usare un formato semplice e naturale - e poi comprimerlo semplicemente con un sistema per scopi generici (come zip, gzip, bzip2, xy o altro).
tl; dr
Ogni piattaforma ha un'architettura, ma puoi creare un numero sostanzialmente illimitato di modi diversi di rappresentare i dati. Non è ragionevole per nessuna lingua fornire un numero illimitato di tipi di dati integrati. Pertanto, C ++ fornisce accesso implicito all'insieme naturale nativo della piattaforma di tipi di dati e consente di codificare personalmente qualsiasi altra rappresentazione (non nativa).
unsingedvalore più grande , che può essere rappresentato con 1 byte è255. 2) Considerare l'overhead del calcolo delle dimensioni ottimali di archiviazione e della riduzione / espansione dell'area di archiviazione di una variabile al variare del valore.