È buona norma utilizzare tipi di dati più piccoli per le variabili per risparmiare memoria?


32

Quando ho imparato il linguaggio C ++ per la prima volta ho imparato che oltre a int, float ecc. Esistevano versioni più piccole o più grandi di questi tipi di dati all'interno del linguaggio. Ad esempio, potrei chiamare una variabile x

int x;
or 
short int x;

La differenza principale è che short int richiede 2 byte di memoria mentre int richiede 4 byte e short int ha un valore inferiore, ma potremmo anche chiamarlo per renderlo ancora più piccolo:

int x;
short int x;
unsigned short int x;

che è ancora più restrittivo.

La mia domanda qui è se è una buona pratica usare tipi di dati separati in base ai valori che la tua variabile assume all'interno del programma. È una buona idea dichiarare sempre le variabili in base a questi tipi di dati?


3
sei a conoscenza del modello di progettazione Flyweight ? "un oggetto che minimizza l'uso della memoria condividendo quanti più dati possibili con altri oggetti simili; è un modo per usare oggetti in grandi numeri quando una semplice rappresentazione ripetuta userebbe una quantità inaccettabile di memoria ..."
moscerino del

5
Con le impostazioni standard del compilatore di imballaggio / allineamento, le variabili saranno comunque allineate ai limiti di 4 byte, quindi potrebbe non esserci alcuna differenza.
Nikie,

36
Caso classico di ottimizzazione prematura.
scarfridge,

1
@nikie - potrebbero essere allineati su un limite di 4 byte su un processore x86 ma questo non è vero in generale. MSP430 posiziona il carattere su qualsiasi indirizzo byte e tutto il resto su un indirizzo byte pari. Penso che AVR-32 e ARM Cortex-M siano uguali.
uɐɪ

3
La seconda parte della tua domanda implica che l'aggiunta in unsignedqualche modo fa sì che un numero intero occupi meno spazio, il che è ovviamente falso. Avrà lo stesso conteggio di valori rappresentabili discreti (dai o prendi 1 a seconda di come è rappresentato il segno) ma si è spostato esclusivamente in positivo.
underscore_d

Risposte:


41

Il più delle volte il costo dello spazio è trascurabile e non dovresti preoccuparti, tuttavia dovresti preoccuparti delle informazioni extra che stai fornendo dichiarando un tipo. Ad esempio, se:

unsigned int salary;

Stai fornendo un'informazione utile ad un altro sviluppatore: lo stipendio non può essere negativo.

La differenza tra short, int, long raramente causerà problemi di spazio nell'applicazione. È più probabile che tu faccia accidentalmente il falso presupposto che un numero si adatti sempre a un tipo di dati. Probabilmente è più sicuro usare sempre int a meno che tu non sia sicuro al 100% che i tuoi numeri siano sempre molto piccoli. Anche allora, è improbabile che ti risparmi una notevole quantità di spazio.


5
È vero che raramente causerà problemi in questi giorni, ma se stai progettando una libreria o una classe che verrà utilizzata da un altro sviluppatore, è un'altra cosa. Forse avranno bisogno di spazio di archiviazione per un milione di questi oggetti, nel qual caso la differenza è grande: 4 MB rispetto a 2 MB solo per questo campo.
dodgy_coder

30
Usare unsignedin questo caso è una cattiva idea: non solo lo stipendio non può essere negativo, ma neanche la differenza tra due stipendi può essere negativa. (In generale, l'utilizzo di unsigned per qualsiasi cosa tranne il bit-twiddling e il comportamento definito in overflow è una cattiva idea.)
zvrba

16
@zvrba: la differenza tra due stipendi non è di per sé uno stipendio ed è quindi legittimo utilizzare un tipo diverso firmato.
JeremyP,

12
@JeremyP Sì, ma se stai usando C (e sembra che ciò sia vero anche in C ++), la sottrazione di numeri interi senza segno si traduce in un int senza segno , che non può essere negativo. Potrebbe trasformarsi nel valore corretto se lo si lancia in un int con segno, ma il risultato del calcolo è un int senza segno. Vedi anche questa risposta per ulteriori stranezze di calcolo con segno / non firmato - motivo per cui non dovresti mai usare variabili senza segno a meno che tu non stia davvero twellando i bit.
Tacroy,

5
@zvrba: la differenza è una quantità monetaria ma non uno stipendio. Ora potresti sostenere che uno stipendio è anche una quantità monetaria (vincolata a numeri positivi e 0 convalidando l'input che è quello che farebbe la maggior parte delle persone) ma la differenza tra due stipendi non è di per sé uno stipendio.
JeremyP,

29

L'OP non ha detto nulla sul tipo di sistema per cui stanno scrivendo i programmi, ma suppongo che l'OP stesse pensando a un tipico PC con GB di memoria dato che si parla di C ++. Come dice uno dei commenti, anche con quel tipo di memoria, se hai diversi milioni di elementi di un tipo - come un array - allora la dimensione della variabile può fare la differenza.

Se entri nel mondo dei sistemi embedded - che non è davvero al di fuori dell'ambito della domanda, poiché l'OP non lo limita ai PC - allora la dimensione dei tipi di dati è molto importante. Ho appena finito un progetto veloce su un microcontrollore a 8 bit che ha solo 8K parole di memoria del programma e 368 byte di RAM. Lì, ovviamente, ogni byte conta. Non si usa mai una variabile più grande del necessario (sia dal punto di vista dello spazio, sia della dimensione del codice - i processori a 8 bit usano molte istruzioni per manipolare i dati a 16 e 32 bit). Perché usare una CPU con risorse così limitate? In grandi quantità, possono costare fino a un quarto.

Attualmente sto facendo un altro progetto integrato con un microcontrollore basato su MIPS a 32 bit che ha 512 KB di memoria flash e 128 KB di RAM (e costa circa $ 6 in quantità). Come per un PC, la dimensione "naturale" dei dati è di 32 bit. Ora diventa più efficiente, dal punto di vista del codice, usare ints per la maggior parte delle variabili invece di caratteri o short. Ma ancora una volta, qualsiasi tipo di array o struttura deve essere considerato se sono garantiti tipi di dati più piccoli. A differenza di compilatori per sistemi più grandi, è più variabili probabile che in una struttura saranno imballati in un sistema embedded. Mi occupo di provare sempre a mettere prima tutte le variabili a 32 bit, poi a 16 bit, quindi a 8 bit per evitare eventuali "buchi".


10
+1 per il fatto che regole diverse si applicano ai sistemi integrati. Il fatto che sia menzionato C ++ non significa che il target sia un PC. Uno dei miei recenti progetti è stato scritto in C ++ su un processore con 32k di RAM e 256K di Flash.
uɐɪ

13

La risposta dipende dal tuo sistema. In generale, ecco i vantaggi e gli svantaggi dell'utilizzo di tipi più piccoli:

vantaggi

  • I tipi più piccoli utilizzano meno memoria sulla maggior parte dei sistemi.
  • I tipi più piccoli consentono calcoli più veloci su alcuni sistemi. Particolarmente vero per float vs double su molti sistemi. E i tipi int più piccoli forniscono anche un codice significativamente più veloce su CPU a 8 o 16 bit.

svantaggi

  • Molte CPU hanno requisiti di allineamento. Alcuni accedono ai dati allineati più velocemente di quelli non allineati. Alcuni devono avere i dati allineati per poter persino accedervi. I tipi interi più grandi equivalgono a un'unità allineata, quindi molto probabilmente non sono disallineati. Ciò significa che il compilatore potrebbe essere costretto a inserire numeri interi più piccoli in numeri più grandi. E se i tipi più piccoli fanno parte di una struttura più grande, è possibile ottenere diversi byte di riempimento inseriti silenziosamente in qualsiasi punto della struttura dal compilatore, per correggere l'allineamento.
  • Conversioni implicite pericolose. C e C ++ hanno diverse regole oscure e pericolose per come le variabili vengono promosse a più grandi, implicitamente senza un typecast. Esistono due serie di regole di conversione implicite intrecciate tra loro, chiamate "regole di promozione dei numeri interi" e "conversioni aritmetiche normali". Leggi di più su di loro qui . Queste regole sono una delle cause più comuni di bug in C e C ++. Puoi evitare molti problemi semplicemente usando lo stesso tipo intero in tutto il programma.

Il mio consiglio è di amare questo:

system                             int types

small/low level embedded system    stdint.h with smaller types
32-bit embedded system             stdint.h, stick to int32_t and uint32_t.
32-bit desktop system              Only use (unsigned) int and long long.
64-bit system                      Only use (unsigned) int and long long.

In alternativa, puoi usare int_leastn_to int_fastn_tda stdint.h, dove n è il numero 8, 16, 32 o 64. int_leastn_ttype significa "Voglio che questo sia almeno n byte ma non mi interessa se il compilatore lo alloca come un tipo più grande per adattarsi all'allineamento ".

int_fastn_t significa "Voglio che sia lungo n byte, ma se renderà il mio codice più veloce, il compilatore dovrebbe usare un tipo più grande di quanto specificato".

Generalmente, i vari tipi di stdint.h sono una pratica molto migliore rispetto al semplice intecc., Perché sono portatili. L'intenzione intera di non dargli una larghezza specificata solo per renderlo portatile. Ma in realtà è difficile portarlo perché non si sa mai quanto sarà grande su un sistema specifico.


Spot sull'allineamento. Nel mio progetto attuale, l'uso gratuito di uint8_t su un MSP430 a 16 bit ha provocato l'arresto anomalo dell'MCU in modi misteriosi (molto probabilmente l'accesso disallineato è avvenuto da qualche parte, forse per colpa di GCC, forse no) - semplicemente sostituendo tutto uint8_t con 'unsigned' ha eliminato gli arresti anomali. L'uso di tipi a 8 bit su archi> 8 bit se non fatale è almeno inefficiente: il compilatore genera ulteriori istruzioni "e reg, 0xff". Utilizzare 'int / unsigned' per la portabilità e liberare il compilatore da vincoli aggiuntivi.
alexei,

11

A seconda di come funziona il sistema operativo specifico, generalmente si prevede che la memoria sia allocata in modo non ottimizzato in modo tale che quando si chiama per un byte, o una parola o un altro tipo di dati di piccole dimensioni da assegnare, il valore occupa un intero registro, tutto è molto proprio. Il modo in cui il tuo compilatore o interprete lavora per interpretare questo è comunque qualcos'altro, quindi se dovessi compilare un programma in C # per esempio, il valore potrebbe occupare fisicamente un registro per se stesso, tuttavia il valore verrà controllato al limite per assicurarti di non farlo provare a memorizzare un valore che supererà i limiti del tipo di dati previsto.

Dal punto di vista delle prestazioni, e se sei davvero pedante su tali cose, è probabilmente più veloce utilizzare semplicemente il tipo di dati che corrisponde maggiormente alla dimensione del registro di destinazione, ma poi perdi tutto quel delizioso zucchero sintattico che rende il lavoro con le variabili così facile .

Come ti aiuta questo? Bene, sta davvero a te decidere per quale tipo di situazione stai codificando. Per quasi tutti i programmi che abbia mai scritto, è sufficiente affidarsi al proprio compilatore per ottimizzare le cose e utilizzare il tipo di dati che è più utile per te. Se è necessaria un'elevata precisione, utilizzare i tipi di dati in virgola mobile più grandi. Se lavori solo con valori positivi, puoi probabilmente usare un numero intero senza segno, ma per la maggior parte è sufficiente usare semplicemente il tipo di dati int.

Se tuttavia hai dei requisiti di dati molto severi, come la scrittura di un protocollo di comunicazione o una sorta di algoritmo di crittografia, l'utilizzo di tipi di dati controllati dalla portata può rivelarsi molto utile, in particolare se stai cercando di evitare problemi relativi a sovraccarichi / scarichi di dati o valori di dati non validi.

L'unico altro motivo per cui riesco a pensare in cima alla mia testa di usare tipi di dati specifici è quando stai cercando di comunicare l'intento all'interno del tuo codice. Se ad esempio si utilizza una scorciatoia, si sta dicendo ad altri sviluppatori che si stanno permettendo numeri positivi e negativi in ​​un intervallo di valori molto piccolo.


6

Come ha commentato Scarfridge , questo è un

Caso classico di ottimizzazione prematura .

Cercare di ottimizzare l'utilizzo della memoria potrebbe influire su altre aree delle prestazioni e le regole d'oro per l'ottimizzazione sono:

La prima regola di ottimizzazione del programma: non farlo .

La seconda regola di ottimizzazione del programma (solo per esperti!): Non farlo ancora . "

- Michael A. Jackson

Per sapere se ora è il momento di ottimizzare richiede benchmarking e test. Devi sapere dove il tuo codice è inefficiente, in modo da poter indirizzare le tue ottimizzazioni.

Al fine di determinare se la versione ottimizzata del codice è effettivamente migliore dell'implementazione ingenua in un dato momento, è necessario confrontarli fianco a fianco con gli stessi dati.

Inoltre, ricorda che solo perché una data implementazione è più efficiente sull'attuale generazione di CPU, non significa che sarà sempre così. La mia risposta alla domanda La micro-ottimizzazione è importante durante la codifica? espone in dettaglio un esempio dell'esperienza personale in cui un'ottimizzazione obsoleta ha comportato un rallentamento dell'ordine di grandezza.

Su molti processori, gli accessi alla memoria non allineati sono significativamente più costosi degli accessi alla memoria allineati. Inserire un paio di pantaloncini nella tua struttura può significare solo che il tuo programma deve eseguire l'operazione di pack / unpack ogni volta che tocchi uno di questi valori.

Per questo motivo, i compilatori moderni ignorano i tuoi suggerimenti. Come commenta nikie :

Con le impostazioni standard del compilatore di imballaggio / allineamento, le variabili saranno comunque allineate ai limiti di 4 byte, quindi potrebbe non esserci alcuna differenza.

Secondo, indovina il compilatore a tuo rischio e pericolo.

C'è un posto per tali ottimizzazioni, quando si lavora con set di dati terabyte o microcontroller incorporati, ma per la maggior parte di noi non è davvero un problema.


3

La differenza principale è che short int richiede 2 byte di memoria mentre int richiede 4 byte e short int ha un valore inferiore, ma potremmo anche chiamarlo per renderlo ancora più piccolo:

Questo non è corretto Non è possibile fare ipotesi sul numero di byte di ciascun tipo, oltre a charessere un byte e almeno 8 bit per byte, con dimensioni di ciascun tipo maggiori o uguali alla precedente.

I vantaggi in termini di prestazioni sono incredibilmente minuscoli per le variabili dello stack: probabilmente saranno comunque allineati / imbottiti.

A causa di questo, shorte longnon hanno praticamente nessun uso al giorno d'oggi, e si è quasi sempre meglio utilizzare int.


Naturalmente, c'è anche quello stdint.hche è perfettamente bene usare quando intnon lo taglia. Se hai mai allocato enormi matrici di numeri interi / strutture, allora ha un intX_tsenso in quanto puoi essere efficiente e fare affidamento sulla dimensione del tipo. Questo non è affatto prematuro in quanto è possibile salvare megabyte di memoria.


1
In realtà, con l'avvento degli ambienti a 64 bit, longpotrebbe essere diverso int. Se il tuo compilatore è LP64, intè 32 bit e long64 bit e scoprirai che intpotrebbe essere ancora allineato a 4 byte (il mio compilatore, ad esempio).
JeremyP,

1
@JeremyP Sì, ho detto diversamente o qualcosa del genere?
Pubblico

La tua ultima frase che afferma breve e lungo non ha praticamente alcun vantaggio. Long sicuramente ha un uso, se non altro come il tipo base diint64_t
JeremyP,

@JeremyP: puoi vivere bene con int e molto a lungo.
gnasher729,

@ gnasher729: Cosa usi se hai bisogno di una variabile che può contenere valori superiori a 65 mila, ma mai fino a un miliardo? int32_t, int_fast32_te longsono tutte buone opzioni, long longè solo dispendioso e intnon portatile.
Ben Voigt,

3

Questo sarà da una sorta di punto di vista OOP e / o imprenditoriale / applicativo e potrebbe non essere applicabile in determinati campi / domini, ma voglio in qualche modo sollevare il concetto di ossessione primitiva .

È consigliabile utilizzare tipi di dati diversi per diversi tipi di informazioni nella propria applicazione. Tuttavia, probabilmente NON è una buona idea utilizzare i tipi integrati per questo, a meno che non si abbiano seri problemi di prestazioni (che sono stati misurati, verificati e così via).

Se vogliamo modellare le temperature in Kelvin nella nostra applicazione, POTREBBE usare un ushortoo uintqualcosa di simile per indicare che "la nozione di gradi negativi Kelvin è assurda e un errore logico di dominio". L'idea alla base di questo è sana, ma non stai andando fino in fondo. Quello che abbiamo capito è che non possiamo avere valori negativi, quindi è utile se possiamo fare in modo che il compilatore si assicuri che nessuno assegni un valore negativo a una temperatura di Kelvin. È anche vero che non è possibile eseguire operazioni bit per bit sulle temperature. E non puoi aggiungere una misura di peso (kg) a una temperatura (K). Ma se modifichi sia la temperatura che la massa come uints, possiamo fare proprio questo.

L'uso di tipi integrati per modellare le nostre entità DOMAIN è destinato a portare ad un codice disordinato e ad alcuni controlli mancanti e invarianti rotti. Anche se un tipo acquisisce ALCUNE parti dell'entità (non può essere negativo), è destinato a perdere altri (non può essere utilizzato in espressioni aritmetiche arbitrarie, non può essere trattato come una matrice di bit, ecc.)

La soluzione è definire nuovi tipi che incapsulano gli invarianti. In questo modo puoi assicurarti che il denaro sia denaro e le distanze siano distanze, e non puoi sommarle insieme e non puoi creare una distanza negativa, ma PUOI creare una quantità di denaro negativa (o un debito). Naturalmente, questi tipi useranno internamente i tipi integrati, ma questo è nascosto dai client. In relazione alla tua domanda su prestazioni / consumo di memoria, questo genere di cose può permetterti di cambiare il modo in cui le cose sono memorizzate internamente senza cambiare l'interfaccia delle tue funzioni che operano sulle tue entità di dominio, se dovessi scoprire che dannazione, a shortè semplicemente troppo dannata grande.


1

Sì, naturalmente. È una buona idea usare uint_least8_tper dizionari, array di costanti enormi, buffer, ecc. È meglio usarlo uint_fast8_tper scopi di elaborazione.

uint8_least_t(archiviazione) -> uint8_fast_t(elaborazione) -> uint8_least_t(archiviazione).

Ad esempio, stai prendendo il simbolo da 8 bit source, i codici da 16 bit dictionariese circa 32 bit constants. Quindi stai elaborando operazioni a 10-15 bit con loro e genera 8 bit destination.

Immaginiamo che devi elaborare 2 gigabyte di source. La quantità di operazioni di bit è enorme. Riceverai un grande bonus di prestazione se passerai ai tipi veloci durante l'elaborazione. I tipi veloci possono essere diversi per ogni famiglia di CPU. È possibile includere stdint.he utilizzare uint_fast8_t, uint_fast16_t, uint_fast32_t, etc.

È possibile utilizzare uint_least8_tinvece che uint8_tper la portabilità. Ma nessuno in realtà sa quale CPU moderna utilizzerà questa funzione. La macchina VAC è un pezzo da museo. Quindi forse è eccessivo.


1
Mentre potresti avere un punto con i tipi di dati che hai elencato, dovresti spiegare perché sono migliori piuttosto che dichiarare che lo sono. Per le persone come me che non hanno familiarità con questi tipi di dati, ho dovuto cercarli su Google per capire di cosa stai parlando.
Peter M,
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.