Perché sono così tanti i numeri che vedo firmati quando non dovrebbero essere? [duplicare]

26

Vedo e lavoro con un sacco di software, scritto da un gruppo abbastanza grande di persone. Molte volte, vedo errate dichiarazioni di tipo intero. Due esempi che vedo più spesso: creare un intero con segno normale quando non ci possono essere numeri negativi. Il secondo è che spesso la dimensione del numero intero è dichiarata come una parola a 32 bit quando molto più piccolo potrebbe fare il trucco. Mi chiedo se il secondo abbia a che fare con l'allineamento delle parole del compilatore che si allinea ai 32 bit più vicini, ma non sono sicuro che questo sia vero nella maggior parte dei casi.

Quando crei un numero, di solito lo crei tenendo a mente le dimensioni, o semplicemente crei qualsiasi cosa sia l'impostazione predefinita "int"?

edit - Votato per riaprire, poiché non penso che le risposte trattino adeguatamente con linguaggi che non sono C / C ++, e che i "duplicati" siano tutti di base in C / C ++. Non riescono a rivolgersi a linguaggi strongmente tipizzati come Ada, dove non possono esserci bug dovuti a tipi non corrispondenti ... non verrà compilato, o se non può essere catturato in fase di compilazione, genererà un'eccezione. Ho intenzionalmente lasciato out nominando C / C ++ in modo specifico, perché altri linguaggi trattano diversi interi in modo molto diverso, anche se la maggior parte delle risposte sembra essere basata su come agiscono i compilatori C / C ++.

    
posta prelic 17.07.2017 - 03:39
fonte

8 risposte

62

Do you see the same thing?

Sì, la stragrande maggioranza dei numeri interi dichiarati è int .

Why?

  1. I valori nativi sono le dimensioni con cui il processore esegue la matematica con *. Rendendoli più piccoli non si ottiene alcuna prestazione (nel caso generale). Rendendoli più grandi significa che forse (a seconda del processore) non possono essere elaborati atomicamente, portando a potenziali bug di concorrenza.
  2. 2 miliardi e il cambiamento è abbastanza grande da ignorare i problemi di overflow per la maggior parte degli scenari. I tipi più piccoli significano più lavoro per affrontarli, e molti più lavoro se indovini sbagliato e devi refactoring per un tipo più grande.
  3. È un problema affrontare la conversione quando hai tutti i tipi di tipi numerici. Le biblioteche usano gli inte I clienti usano gli inte I server usano gli inte L'interoperabilità diventa più difficile, perché la serializzazione spesso assume valori intrinseci - se i tuoi contratti non corrispondono, improvvisamente ci sono piccoli bug che si verificano quando serializzano un int e tu deserializzi un uint .

In breve, non c'è molto da guadagnare, e alcuni aspetti negativi non banali. E francamente, preferisco passare il mio tempo a pensare ai problemi real quando sto codificando, non a quale tipo di numero usare.

* - in questi giorni, la maggior parte dei personal computer è a 64 bit, ma i dispositivi mobili sono più difficili.

    
risposta data 17.07.2017 - 04:16
fonte
19

Per quanto riguarda le dimensioni, stai operando sotto l'impressione sbagliata che "più piccolo è meglio", che semplicemente non è vero.

Anche se ignoriamo completamente problemi come il tempo del programmatore o la propensione all'errore, i tipi di interi più piccoli possono comunque avere i seguenti svantaggi.

Tipi più piccoli = lavoro più grande

I processori non funzionano su dati di dimensioni arbitrarie; eseguono operazioni in registri di dimensioni specifiche. Provare a fare aritmetica con precisione inferiore di quella memorizzata nei registri può facilmente richiedere di fare un lavoro extra .

Ad esempio, se un programma C esegue l'aritmetica in uint8_t - un tipo intero a 8 bit senza segno dove è specificato l'overflow come riduzione modulo 256 - quindi a meno che il processore non abbia istruzioni di assemblaggio specializzate per gestire il caso speciale, il programma dovrà seguire ogni operazione aritmetica con una maschera di 0xff , a meno che il compilatore non sia in grado di provare dimostrando che la maschera non è necessaria.

Tipi più piccoli = memoria inefficiente

La memoria non è uniforme. È abbastanza comune sui processori che l'accesso alla memoria su indirizzi multipli di 4 byte (o più!) Sia molto più efficiente dell'accesso alla memoria su altri indirizzi.

Potresti pensare che usare un campo di 1 byte invece di un campo di 4 byte ti stia aiutando, ma la realtà potrebbe essere che in realtà ti danneggia a causa di tale disallineamento gli accessi alla memoria sono più lenti del necessario.

Naturalmente, i compilatori sanno tutto su questo, e in molti posti inseriranno lo spazio sprecato necessario per rendere le cose più veloci:

struct this_struct_is_64_bits_not_40_bits
{
    uint32_t x; uint8_t y;
};

Numeri interi firmati = più opportunità di ottimizzazione

Una particolarità di C e C ++ è che l'overflow di interi con segno è comportamento indefinito , che consente al compilatore di effettuare ottimizzazioni senza riguardo all'effetto che l'ottimizzazione potrebbe avere in caso di overflow.

Spesso le guide per l'ottimizzazione raccomandano decisamente l'uso di numeri interi con segno in molti posti proprio per questo motivo. Ad esempio, dalle Best Practice CUDA Guida

Note:Low Medium Priority: Use signed integers rather than unsigned integers as loop counters.

    
risposta data 17.07.2017 - 10:12
fonte
18

L'utilizzo dell'intestazione a 32 bit con segno "funziona correttamente" in tutti questi casi:

  • Loop
  • Intero aritmetico
  • Indicizzazione e dimensionamento delle matrici
  • Valori di enumerazione
  • Dimensione degli oggetti in memoria (cose di dimensioni ragionevoli)
  • Dimensioni dell'immagine (immagini di dimensioni ragionevoli)

Sì, non tutti gli usi richiedono segnaletica o 32 bit di dati, ma la compatibilità di int 32 firmato con la maggior parte dei casi d'uso lo rende una scelta facile da fare. Prendendo in considerazione qualsiasi altro tipo intero prenderebbe in considerazione che la maggior parte delle persone non vuole prendersi il tempo necessario. E con la disponibilità di memoria oggi ci godiamo il lusso di sprecare pochi byte qua e là. La standardizzazione su un tipo intero comune rende la vita di tutti un po 'più semplice, e la maggior parte delle librerie utilizza in modo predefinito numeri interi a 32 bit, quindi scegliere di utilizzare altri tipi di interi sarebbe una seccatura da un punto di fusione / conversione.

    
risposta data 17.07.2017 - 04:04
fonte
10

Ci sono ancora molti milioni, o miliardi, di dispositivi di elaborazione incorporati là fuori dove il numero intero "predefinito" è 16 bit, otto bit, (alcuni ancora più piccoli), dove l'assunto che un intero con segno sia sufficiente non è un ipotesi valida. (Lavoro con loro tutto il tempo).

Se hai a che fare con qualsiasi tipo di protocollo di comunicazione, dovresti riflettere:

  • Dimensioni, (8 bit, 16, 32, 64, altri),
  • Firmato / Unsigned
  • Endianness
  • Imballaggio / Allineamento

Quindi, mentre vedo le persone che utilizzano int ovunque nel mio campo di lavoro, abbiamo regole specifiche contro di esso (MISRA) e progettiamo deliberatamente i nostri protocolli di comunicazione, tipo e archivi dati con le insidie in mente e devi rifiutare tale codice prima che entri nel codice di produzione.

    
risposta data 17.07.2017 - 08:45
fonte
5

Vorrei pubblicare una risposta che va nella direzione opposta alla maggior parte degli altri. Suppongo che usare int per tutto non sia buono, almeno in C o C ++.

  1. int non ha molto significato semantico. Usando un linguaggio tipizzato rigorosamente dovresti comunicare il maggior numero di significati possibili con i tuoi tipi. Quindi, se la tua variabile rappresenta un valore per il quale non ha senso essere negativo, perché non lo trasmetti usando unsigned int ?
  2. Simile a quanto sopra, sono disponibili tipi ancora più precisi di int e unsigned int : in C, la dimensione di un oggetto dovrebbe essere size_t , l'offset di un puntatore dovrebbe essere ptrdiff_t , ecc. Verranno tutti convertiti in veri e propri tipi di int dal compilatore, ma trasmetteranno alcune informazioni utili aggiuntive.
  3. I tipi precisi possono consentire un'ottimizzazione specifica dell'architettura (ad esempio uint_fast32_t in C).
  4. Normalmente, un processore a 64 bit può operare su un valore a 64 bit alla volta o su due valori a 32 bit. In altre parole, in un ciclo di clock, è possibile ad esempio eseguire 1 somma a 64 bit o due somme a 32 bit. Questo raddoppia efficacemente la maggior parte delle operazioni matematiche se gli interi a 32 bit sono sufficienti per te. (Non riesco a trovare una citazione di testo per questo, ma iirc è stato detto da Alexei Alexandrescu in un discorso di CppCon che sarebbe diventato fonte abbastanza autorevole).
  5. Se si utilizza un numero intero non assegnato a 32 bit anziché un numero intero con segno a 64 bit, per un valore che può comunque essere solo positivo, si è effettivamente dimezzata la memoria richiesta per contenere quel valore. Potrebbe non essere così importante nel grande schema delle cose, se si pensa a come la RAM a basso costo è al giorno d'oggi sulla maggior parte delle piattaforme, ma può fare la differenza se si raddoppia la quantità di dati che va nella cache L1, per esempio!
risposta data 17.07.2017 - 13:41
fonte
1

Ci sono diversi motivi per cui di solito è leggermente più semplice usare i numeri firmati in C durante i calcoli. Ma questi sono solo consigli che si applicano a calcoli / loop in linguaggi di tipo C, non per progettare i tipi di dati astratti, i protocolli di comunicazione e tutto ciò che riguarda lo storage.

  1. Nel 99% dei casi, le tue variabili funzioneranno molto più vicino a zero rispetto al valore MAX_INT , e in questi casi l'uso di un int firmato rende spesso più semplice garantire la correttezza:

    // if i is unsigned this will loop forever
    // due to underflow to (unsigned)-1
    while (--i >= 0)
    { /* do something */ }
    
  2. La promozione di interi in C è una regola che tenta di promuovere tutti gli operandi più piccoli di int in un int (firmato), se si adattano. Ciò significa che le tue variabili unsigned più piccole ( uint8_t o uint16_t ) saranno trattate come int durante le operazioni:

    uint8_t x = 1;
    uint8_t y = 2;
    
    // this will produce a warning, because the 
    // result of 'x + y' is an 'int', and you're
    // placing it into a 'uint8_t' without explicitly
    // casting:
    
    uint8_t result = x + y;
    

    Allo stesso tempo, usando tipi più piccoli, non hai probabilmente guadagnato nulla in termini di prestazioni, perché i compilatori in genere scelgono int per corrispondere alla dimensione della parola dell'architettura di destinazione, quindi i registri della CPU non sono molto utili se stai usando qualcosa di più piccolo.

Ovviamente, questo non significa che sprecherai spazio nei campi struct su 32-bit int s, se tutto ciò che ti serve è un uint8_t .

    
risposta data 17.07.2017 - 10:20
fonte
0

Per quanto riguarda la cosa firmata / non firmata: Ricorda che l'aritmetica non firmata ha semantica totalmente diversa rispetto all'aritmetica firmata. L'aritmetica senza segno è mod 2 ^ n (dove n è il numero di bit del tuo tipo senza segno). Tuttavia, tale aritmetica è spesso indesiderata ed è meglio gestire un overflow come errore.

Per quanto riguarda il C ++, si noti anche che esiste qualche rammarico nel comitato degli standard sull'uso dei tipi di dati non firmati su tutta la libreria standard. Vedi questo video alle 9:50, 42:40, 1: 02:50.

    
risposta data 17.07.2017 - 09:57
fonte
0

firmato contro senza segno o 16-bit contro 32-bit sono solo alcuni dei casi di specifica limiti esatti per le variabili intere.

C non ha modo di specificare questi limiti, come in Ada:

subtype My_Index is Integer range 2 .. 7;

In C, int , breve , char , lungo , non firmato , sono solo un modo conveniente per ottimizzare le dimensioni dello spazio di archiviazione. Non sono destinati a portare una semantica rigorosa.

    
risposta data 17.07.2017 - 11:21
fonte

Leggi altre domande sui tag