L'operazione bit a bit comporta dimensioni variabili impreviste


24

Contesto

Stiamo eseguendo il porting del codice C che è stato originariamente compilato utilizzando un compilatore C a 8 bit per il microcontrollore PIC. Un linguaggio comune che è stato utilizzato per impedire che le variabili globali senza segno (ad esempio i contatori di errori) si spostassero indietro a zero è il seguente:

if(~counter) counter++;

L'operatore bit a bit qui inverte tutti i bit e l'istruzione è vera solo se counterè inferiore al valore massimo. È importante sottolineare che funziona indipendentemente dalla dimensione della variabile.

Problema

Ora stiamo prendendo di mira un processore ARM a 32 bit utilizzando GCC. Abbiamo notato che lo stesso codice produce risultati diversi. Per quanto ne sappiamo, sembra che l'operazione di complemento bit a bit restituisca un valore di dimensioni diverse da quanto ci aspetteremmo. Per riprodurlo, compiliamo, in GCC:

uint8_t i = 0;
int sz;

sz = sizeof(i);
printf("Size of variable: %d\n", sz); // Size of variable: 1

sz = sizeof(~i);
printf("Size of result: %d\n", sz); // Size of result: 4

Nella prima riga di output, otteniamo ciò che ci aspetteremmo: iè 1 byte. Tuttavia, il complemento bit per bit di in irealtà è di quattro byte che causa un problema perché i confronti con questo ora non daranno i risultati previsti. Ad esempio, se lo fai (dove iè correttamente inizializzato uint8_t):

if(~i) i++;

Vedremo i"avvolgere" da 0xFF a 0x00. Questo comportamento è diverso in GCC rispetto a quando funzionava come previsto nel precedente compilatore e microcontrollore PIC a 8 bit.

Siamo consapevoli di poterlo risolvere lanciando in questo modo:

if((uint8_t)~i) i++;

Oppure, di

if(i < 0xFF) i++;

Tuttavia, in entrambe queste soluzioni alternative, la dimensione della variabile deve essere nota ed è soggetta a errori per lo sviluppatore del software. Questi tipi di controlli sui limiti superiori si verificano in tutta la base di codice. Esistono più dimensioni di variabili (ad es.,uint16_t E unsigned charcosì via) e la modifica di questi in una base di codice altrimenti di lavoro non è qualcosa che stiamo guardando al futuro.

Domanda

La nostra comprensione del problema è corretta e ci sono opzioni disponibili per risolvere questo problema che non richiedono di visitare nuovamente ogni caso in cui abbiamo usato questo linguaggio? La nostra ipotesi è corretta, secondo cui un'operazione come il complemento bit per bit dovrebbe restituire un risultato delle stesse dimensioni dell'operando? Sembra che questo si rompa, a seconda delle architetture del processore. Mi sento come se stessi prendendo pillole pazze e che C dovrebbe essere un po 'più portatile di così. Ancora una volta, la nostra comprensione di questo potrebbe essere sbagliata.

In apparenza questo potrebbe non sembrare un grosso problema, ma questo idioma precedentemente funzionante viene utilizzato in centinaia di località e non vediamo l'ora di capirlo prima di procedere con costose modifiche.


Nota: qui c'è una domanda duplicata apparentemente simile ma non esatta: L' operazione bit a bit su char dà un risultato a 32 bit

Non ho visto il vero punto cruciale del problema discusso qui, vale a dire, la dimensione del risultato di un complemento bit per bit è diversa da quella passata nell'operatore.


14
"La nostra ipotesi è corretta, secondo cui un'operazione come il complemento bit per bit dovrebbe restituire un risultato delle stesse dimensioni dell'operando?" No, questo non è corretto, si applicano le promozioni intere.
Thomas Jager,

2
Sebbene certamente rilevanti, non sono convinto che siano duplicati di questa particolare domanda, perché non forniscono una soluzione al problema.
Cody Grey

3
Mi sento come se stessi prendendo pillole pazze e che C dovrebbe essere un po 'più portatile di così. Se non hai ricevuto promozioni per interi su tipi a 8 bit, il tuo compilatore non era compatibile con lo standard C. In tal caso, penso che dovresti esaminare tutti i calcoli per controllarli e correggerli se necessario.
user694733

1
Sono l'unico che mi chiedo quale logica, a parte contatori davvero poco importanti, può portarla ad "incrementare se c'è abbastanza spazio, altrimenti dimenticalo"? Se stai eseguendo il porting del codice, puoi usare int (4 byte) invece di uint_8? Ciò impedirebbe il problema in molti casi.
disco

1
@puck Hai ragione, potremmo cambiarlo in 4 byte, ma si romperebbe la compatibilità quando si comunica con i sistemi esistenti. L'intento è quello di sapere quando ci sono eventuali errori e quindi un contatore 1 byte originariamente era sufficiente, e rimane così.
Charlie Salts,

Risposte:


26

Quello che vedi è il risultato di promozioni intere . Nella maggior parte dei casi in cui viene utilizzato un valore intero in un'espressione, se il tipo di valore è inferiore intal valore viene promosso int. Questo è documentato nella sezione 6.3.1.1p2 della norma C :

Quanto segue può essere usato in un'espressione ovunque sia into unsigned intpuò essere usato

  • Un oggetto o un'espressione con un tipo intero (diverso da into unsigned int) il cui rango di conversione intero è inferiore o uguale al rango di inte unsigned int.
  • Un campo bit di tipo _Bool, int ,firmato int , orunsigned int`.

Se un intpuò rappresentare tutti i valori del tipo originale (come limitato dalla larghezza, per un campo bit), il valore viene convertito in un int; in caso contrario, viene convertito in un unsigned int. Queste sono chiamate promozioni intere . Tutti gli altri tipi sono invariati dalle promozioni intere.

Pertanto, se una variabile ha tipo uint8_te valore 255, l'utilizzo di qualsiasi operatore diverso da un cast o assegnazione su di essa la convertirà in tipo intcon il valore 255 prima di eseguire l'operazione. Ecco perché sizeof(~i)ti dà 4 invece di 1.

La sezione 6.5.3.3 descrive che le promozioni di numeri interi si applicano ~all'operatore:

Il risultato ~dell'operatore è il complemento bit a bit dell'operando (promosso) (ovvero, ogni bit nel risultato viene impostato se e solo se non è impostato il bit corrispondente nell'operando convertito). Le promozioni intere vengono eseguite sull'operando e il risultato ha il tipo promosso. Se il tipo promosso è un tipo senza segno, l'espressione ~Eè equivalente al valore massimo rappresentabile in quel tipo meno E.

Quindi, supponendo un 32 bit int, se counterha il valore di 8 bit 0xff, viene convertito nel valore di 32 bit 0x000000ffe applicarlo ~ad esso ti dà 0xffffff00.

Probabilmente il modo più semplice per gestirlo è senza dover conoscere il tipo è controllare se il valore è 0 dopo l'incremento e in tal caso diminuirlo.

if (!++counter) counter--;

Il wrapping di numeri interi senza segno funziona in entrambe le direzioni, quindi decrementando un valore di 0 si ottiene il valore positivo più grande.


1
if (!++counter) --counter;potrebbe essere meno strano per alcuni programmatori rispetto all'uso dell'operatore virgola.
Eric Postpischil,

1
Un'altra alternativa è ++counter; counter -= !counter;.
Eric Postpischil,

@EricPostpischil In realtà, mi piace di più la tua prima opzione. Modificato.
dbush,

15
Questo è brutto e illeggibile, non importa come lo scrivi. Se devi usare un linguaggio come questo, fai un favore a tutti i programmatori di manutenzione e avvolgilo come una funzione in linea : qualcosa come increment_unsigned_without_wraparoundo increment_with_saturation. Personalmente, userei una generica funzione tri-operando clamp.
Cody Grey

5
Inoltre, non puoi renderla una funzione, perché deve comportarsi diversamente per diversi tipi di argomenti. Dovresti usare una macro generica di tipo .
user2357112 supporta Monica il

7

in dimensione di (i); richiedi la dimensione della variabile i , quindi 1

in sizeof (~ i); richiedi la dimensione del tipo dell'espressione, che è un int , nel tuo caso 4


Usare

if (~ i)

di sapere se i non valore 255 (nel tuo caso con l'uint8_t) non è molto leggibile, basta fare

if (i != 255)

e avrai un codice portatile e leggibile


Esistono più dimensioni di variabili (ad es. Uint16_t e caratteri senza segno, ecc.)

Per gestire qualsiasi dimensione di unsigned:

if (i != (((uintmax_t) 2 << (sizeof(i)*CHAR_BIT-1)) - 1))

L'espressione è costante, quindi calcolata al momento della compilazione.

#include <limits.h> per CHAR_BIT e #include <stdint.h> per uintmax_t


3
La domanda afferma esplicitamente che hanno più dimensioni da affrontare, quindi != 255è inadeguata.
Eric Postpischil,

@EricPostpischil ah sì, lo dimentico, quindi "if (i! = ((1u << sizeof (i) * 8) - 1))" supponendo sempre non firmato?
bruno,

1
Ciò non sarà definito per gli unsignedoggetti poiché gli spostamenti dell'intera larghezza dell'oggetto non sono definiti dallo standard C, ma possono essere corretti con (2u << sizeof(i)*CHAR_BIT-1) - 1.
Eric Postpischil,

oh sì, spesso, CHAR_BIT, mio ​​cattivo
bruno

2
Per sicurezza con tipi più ampi, si potrebbe usare ((uintmax_t) 2 << sizeof(i)*CHAR_BIT-1) - 1.
Eric Postpischil,

5

Ecco alcune opzioni per l'implementazione di "Aggiungi 1 xma blocca al massimo valore rappresentabile", dato che si xtratta di un tipo intero senza segno:

  1. Aggiungi uno se e solo se xè inferiore al valore massimo rappresentabile nel suo tipo:

    x += x < Maximum(x);

    Vedere la voce seguente per la definizione di Maximum. Questo metodo ha buone probabilità di essere ottimizzato da un compilatore per istruzioni efficienti come un confronto, una qualche forma di set o spostamento condizionali e un'aggiunta.

  2. Confronta con il valore più grande del tipo:

    if (x < ((uintmax_t) 2u << sizeof x * CHAR_BIT - 1) - 1) ++x

    (Questo calcola 2 N , dove N è il numero di bit in x, spostando 2 di N −1 bit. Facciamo questo invece di spostare 1 N bit perché uno spostamento del numero di bit in un tipo non è definito dalla C standard. La CHAR_BITmacro potrebbe non avere familiarità con alcuni; è il numero di bit in un byte, quindi sizeof x * CHAR_BITè il numero di bit nel tipo di x.)

    Questo può essere racchiuso in una macro come desiderato per estetica e chiarezza:

    #define Maximum(x) (((uintmax_t) 2u << sizeof (x) * CHAR_BIT - 1) - 1)
    if (x < Maximum(x)) ++x;
  3. Incrementa xe correggi se si sposta a zero, usando un if:

    if (!++x) --x; // !++x is true if ++x wraps to zero.
  4. Incrementa xe correggi se si sposta a zero, usando un'espressione:

    ++x; x -= !x;

    Questo è nominalmente senza rami (a volte vantaggioso per le prestazioni), ma un compilatore può implementarlo come sopra, usando un ramo se necessario ma possibilmente con istruzioni incondizionate se l'architettura di destinazione ha istruzioni adeguate.

  5. Un'opzione senza rami, utilizzando la macro sopra, è:

    x += 1 - x/Maximum(x);

    Se xè il massimo del suo tipo, viene valutato x += 1-1. Altrimenti lo è x += 1-0. Tuttavia, la divisione è piuttosto lenta su molte architetture. Un compilatore può ottimizzare questo in istruzioni senza divisione, a seconda del compilatore e dell'architettura di destinazione.


1
Non riesco proprio a valutare una risposta che mi consiglia di utilizzare una macro. C ha funzioni incorporate. Non stai facendo nulla all'interno di quella definizione macro che non può essere facilmente eseguita all'interno di una funzione incorporata. E se hai intenzione di usare una macro, assicurati di mettere tra parentesi strategicamente la chiarezza: l'operatore << ha una precedenza molto bassa. Clang lo avverte con -Wshift-op-parentheses. La buona notizia è che un compilatore ottimizzato non genererà una divisione qui, quindi non devi preoccuparti che sia lento.
Cody Grey

1
@CodyGray, se pensi di poterlo fare con una funzione, scrivi una risposta.
Carsten S

2
@CodyGray: sizeof xnon può essere implementato all'interno di una funzione C perché xdovrebbe essere un parametro (o altra espressione) con un tipo fisso. Non è stato possibile produrre la dimensione di qualsiasi tipo di argomento utilizzato dal chiamante. Una lattina per macro.
Eric Postpischil,

2

Prima di stdint.h le dimensioni delle variabili possono variare da compilatore a compilatore e i tipi di variabili effettivi in ​​C sono ancora int, long, ecc. E sono ancora definiti dall'autore del compilatore per quanto riguarda le loro dimensioni. Non alcuni presupposti standard né target specifici. Gli autori devono quindi creare stdint.h per mappare i due mondi, questo è lo scopo di stdint.h per mappare uint_this che a int, long, short.

Se stai eseguendo il porting del codice da un altro compilatore e utilizza char, short, int, long, devi passare attraverso ogni tipo e fare la porta da solo, non c'è modo di aggirarlo. E o si finisce con la giusta dimensione per la variabile, la dichiarazione cambia ma il codice come scritto funziona ...

if(~counter) counter++;

oppure ... fornisci direttamente la maschera o il typecast

if((~counter)&0xFF) counter++;
if((uint_8)(~counter)) counter++;

Alla fine della giornata, se vuoi che questo codice funzioni, devi portarlo sulla nuova piattaforma. La tua scelta su come. Sì, devi dedicare del tempo a risolvere ogni caso e farlo nel modo giusto, altrimenti continuerai a tornare a questo codice che è ancora più costoso.

Se si isolano i tipi di variabili sul codice prima del porting e le dimensioni dei tipi di variabili, isolare le variabili che lo fanno (dovrebbe essere facile da grep) e modificare le loro dichiarazioni usando le definizioni stdint.h che si spera non cambieranno in futuro, e rimarrai sorpreso, ma a volte vengono utilizzate le intestazioni sbagliate, quindi anche effettuare controlli in modo da poter dormire meglio la notte

if(sizeof(uint_8)!=1) return(FAIL);

E mentre quello stile di codifica funziona (if (~ counter) counter ++;), per la portabilità desidera ora e in futuro è meglio usare una maschera per limitare specificamente le dimensioni (e non fare affidamento sulla dichiarazione), farlo quando il codice viene scritto in primo luogo o semplicemente termina la porta e quindi non dovrai ripetere il porting un altro giorno. O per rendere il codice più leggibile, allora fai if <<0xFF allora o x! = 0xFF o qualcosa del genere, quindi il compilatore può ottimizzarlo nello stesso codice che avrebbe per una di queste soluzioni, rendendolo più leggibile e meno rischioso ...

Dipende da quanto sia importante il prodotto o quante volte vuoi inviare patch / aggiornamenti o rotolare un camion o andare al laboratorio per risolvere il problema se cerchi di trovare una soluzione rapida o tocchi semplicemente le righe di codice interessate. se è solo un centinaio o pochi non è così grande di una porta.


0
6.5.3.3 Operatori aritmetici unari
...
4 Il risultato ~dell'operatore è il complemento bit a bit dell'operando (promosso) (ovvero, ogni bit nel risultato viene impostato se e solo se il bit corrispondente nell'operando convertito non è impostato ). Le promozioni intere vengono eseguite sull'operando e il risultato ha il tipo promosso . Se il tipo promosso è un tipo senza segno, l'espressione ~Eè equivalente al valore massimo rappresentabile in quel tipo meno E.

C 2011 Bozza online

Il problema è che l'operando di ~viene promosso intprima che l'operatore venga applicato.

Sfortunatamente, non penso che ci sia una via d'uscita facile da questo. scrittura

if ( counter + 1 ) counter++;

non aiuterà perché le promozioni si applicano anche lì. L'unica cosa che posso suggerire è la creazione di alcune costanti simboliche per il valore massimo che si desidera rappresentare e testare l'oggetto:

#define MAX_COUNTER 255
...
if ( counter < MAX_COUNTER-1 ) counter++;

Apprezzo il punto sulla promozione dei numeri interi: sembra che questo sia il problema che stiamo incontrando. Vale la pena sottolineare, tuttavia, che nel tuo secondo esempio di codice, -1non è necessario, in quanto ciò causerebbe la stabilizzazione del contatore a 254 (0xFE). In ogni caso, questo approccio, come menzionato nella mia domanda, non è l'ideale a causa delle diverse dimensioni delle variabili nella base di codice che partecipano a questo linguaggio.
Charlie Salts,
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.