bitlvl
IT

Calcolatore di overflow e aritmetica degli interi

Scrivi un’espressione, scegli un tipo e un linguaggio e guarda esattamente cosa fa la macchina: quali operandi vengono promossi o convertiti, dove il risultato smette di starci, se è wraparound, un panic o comportamento indefinito, e i bit prima e dopo.

I risultati e i messaggi di questo strumento sono mostrati in inglese.

Perché gli interi sorprendono

Un intero di macchina ha un numero fisso di bit, quindi qualsiasi operazione può produrre un valore che non ci sta. Cosa succede allora non lo decide l’hardware (che semplicemente gira), ma il linguaggio:

LinguaggioOverflow con segnoOverflow senza segnoScorrimento ≥ larghezzaDivisione per zero
C / C++comportamento indefinito: l’ottimizzatore assume che non accada maigira modulo 2ⁿ (definito)indefinitoindefinito (SIGFPE su x86)
Rustpanic in debug, giro in release; checked_/wrapping_/saturating_/overflowing_ sono espliciticome con segnopanic in debugpanic
Gogira (definito)gira0 / −1, nessun panicpanic
Javagira (definito); Math.addExact lancianessun tipo senza segno (char è unsigned a 16 bit)contatore mascherato a 5/6 bitArithmeticException
JavaScripti numeri sono double: esatti fino a 2⁵³, poi arrotondati; gli operatori sui bit lavorano su int32contatore mascherato a 5 bitInfinity / NaN

Le regole del C che mordono

  • Promozione degli interi. Tutto ciò che è più piccolo di int diventa int prima dell’aritmetica, quindi uint8_t a = 200, b = 100; a + b fa 300, e solo l’assegnazione di ritorno in un uint8_t lo tronca a 44. ~x su un uint8_t dà un int negativo.
  • Conversioni aritmetiche usuali. Quando un signed incontra un unsigned della stessa larghezza, il lato signed diventa unsigned: -1 > 0u è vero, if (len - 1 < 0) non è mai vero se len è un size_t.
  • L’overflow con segno è UB. if (x + 1 < x) come controllo di overflow viene eliminato da GCC e Clang con l’ottimizzazione, perché possono assumere che x + 1 non vada mai in overflow. Controlla prima dell’operazione (x > INT_MAX - y) o usa __builtin_add_overflow.
  • Scorrimenti. Scorrere della larghezza o più, o scorrere a sinistra un valore negativo, è indefinito; x86 maschera il contatore, ARM dà zero, il compilatore fa ciò che dice il suo modello.
  • Divisione. Tronca verso zero, quindi -7 / 2 == -3 e -7 % 2 == -1; INT_MIN / -1 va in overflow e provoca davvero un trap su x86.

Leggere l’output

Il risultato è mostrato nel tipo in cui finisce, con il suo schema di bit e gli stessi bit nella lettura di segno opposta, cioè ciò che vedi quando un valore attraversa un confine (int)/(unsigned) o viene memorizzato attraverso un altro tipo e riletto. La lista dei passaggi è l’ordine di valutazione con il tipo a ogni passo; le note sono i punti in cui il linguaggio ha fatto qualcosa di diverso dalla semplice aritmetica.

Domande

Perché il risultato in C differisce da ciò che ha stampato il mio programma?

Per il comportamento indefinito non c’è un’unica risposta giusta: questa pagina mostra il valore di wraparound che produce il codice non ottimizzato e segna il passaggio come UB. Con -O2 il compilatore può aver assunto che l’overflow non possa accadere e generato altro codice. Compila con -fsanitize=undefined per vedere il punto esatto.

Cosa fa il selettore del tipo?

Imposta il tipo dei letterali senza suffisso né cast, che in C è int di default. Scegli u8 e scrivi 200 + 100 per vedere Rust andare in panic, o C promuovere a int e riuscire.

Come scrivo un cast?

Stile C: (u8) expr, (uint32_t) expr, (int) expr. Stile Rust: expr as u8. Entrambi funzionano in qualsiasi modalità di linguaggio. INT_MAX, INT_MIN, UINT_MAX, LONG_MAX, SIZE_MAX sono disponibili come nomi.

Gestisce i 128 bit?

Sì: i128 e u128 vengono valutati esattamente (tutta l’aritmetica avviene a precisione arbitraria e poi viene adattata al tipo).

E la virgola mobile?

Non qui; per quella c’è il visualizzatore di float IEEE 754 su questo sito. Questo strumento tratta solo ciò che succede ai numeri interi nei tipi a larghezza fissa.