Perché il comportamento di overflow intero senza segno è definito ma l'overflow di intero senza segno non lo è?


210

L'overflow di numeri interi senza segno è ben definito dagli standard C e C ++. Ad esempio, lo standard C99 ( §6.2.5/9) afferma

Un calcolo che coinvolge operandi senza segno non può mai sovraccaricare, poiché un risultato che non può essere rappresentato dal tipo intero senza segno risultante viene ridotto nel modulo che è uno maggiore del valore più grande che può essere rappresentato dal tipo risultante.

Tuttavia, entrambi gli standard affermano che l'overflow di numeri interi con segno è un comportamento indefinito. Ancora una volta, dallo standard C99 ( §3.4.3/1)

Un esempio di comportamento indefinito è il comportamento sul overflow del numero intero

C'è una ragione storica o (anche meglio!) Una ragione tecnica per questa discrepanza?


50
Probabilmente perché esiste più di un modo di rappresentare numeri interi con segno. In che modo non è specificato nello standard, almeno non in C ++.
juanchopanza,


7
Ciò che ha detto juanchopanza ha senso. A quanto ho capito, lo standard C originale in gran parte codificava la pratica esistente. Se tutte le implementazioni in quel momento erano d'accordo su ciò che dovrebbe fare "overflow" senza segno, questa è una buona ragione per renderlo standardizzato. Non erano d'accordo su cosa dovrebbe fare l'overflow firmato, quindi non sono entrati nello standard.

2
@DavidElliman Anche il wraparound non firmato in aggiunta è facilmente rilevabile ( if (a + b < a)). L'overflow della moltiplicazione è difficile sia per i tipi firmati che per quelli non firmati.

5
@DavidElliman: non è solo un problema se è possibile rilevarlo, ma quale sia il risultato. In un'implementazione segno + valore MAX_INT+1 == -0, mentre su un complemento a due sarebbeINT_MIN
David Rodríguez - dribeas

Risposte:


163

Il motivo storico è che la maggior parte delle implementazioni in C (compilatori) utilizzava semplicemente il comportamento di overflow più semplice da implementare con la rappresentazione intera utilizzata. Le implementazioni C di solito utilizzavano la stessa rappresentazione utilizzata dalla CPU, quindi il comportamento di overflow è seguito dalla rappresentazione intera utilizzata dalla CPU.

In pratica, sono solo le rappresentazioni dei valori firmati che possono differire in base all'implementazione: complemento, complemento a due, magnitudine del segno. Per un tipo senza segno non esiste alcun motivo per cui lo standard consenta la variazione poiché esiste solo una rappresentazione binaria ovvia (lo standard consente solo la rappresentazione binaria).

Citazioni rilevanti:

C99 6.2.6.1:3 :

I valori memorizzati in campi bit senza segno e oggetti di tipo char senza segno devono essere rappresentati utilizzando una pura notazione binaria.

C99 6.2.6.2:2 :

Se il bit di segno è uno, il valore deve essere modificato in uno dei seguenti modi:

- il valore corrispondente con bit di segno 0 viene negato ( segno e magnitudine );

- il bit del segno ha il valore - (2 N ) ( complemento a due );

- il bit del segno ha il valore - (2 N - 1) ( complemento ).


Al giorno d'oggi, tutti i processori usano la rappresentazione del complemento a due, ma l'overflow aritmetico firmato rimane indefinito e i produttori di compilatori vogliono che rimanga indefinito perché usano questa indefinibilità per aiutare con l'ottimizzazione. Vedi ad esempio questo post sul blog di Ian Lance Taylor o questa denuncia di Agner Fog e le risposte alla sua segnalazione di bug.


6
La nota importante qui, tuttavia, è che non esistono architetture nel mondo moderno che usano qualcosa di diverso dall'aritmetica firmata del complemento di 2. Che gli standard linguistici consentano ancora l'implementazione su un PDP-1, ad esempio, è un puro artefatto storico.
Andy Ross,

9
@AndyRoss ma ci sono ancora sistemi (compilatori OS +, certamente con una vecchia storia) con il proprio complemento e nuove versioni a partire dal 2013. Un esempio: OS 2200.
ouah

3
@Andy Ross considereresti "nessuna architettura ... usando qualcosa di diverso dal complemento di 2 ..." oggi include la gamma di DSP e processori integrati?
chux - Ripristina Monica il

11
@AndyRoss: Mentre non ci sono architetture “no” che usano qualcosa di diverso dal complemento 2s (per qualche definizione di “no”), ci sono sicuramente architetture DSP che usano l'aritmetica satura per interi con segno.
Stephen Canon,

10
La saturazione dell'aritmetica firmata è decisamente conforme allo standard. Naturalmente le istruzioni di wrapping devono essere utilizzate per l'aritmetica senza segno, ma il compilatore ha sempre le informazioni per sapere se viene eseguita l'aritmetica senza segno o firmata, quindi può certamente scegliere le istruzioni in modo appropriato.
Caf

15

A parte la buona risposta di Pascal (che sicuramente è la motivazione principale), è anche possibile che alcuni processori causino un'eccezione sull'overflow di numeri interi con segno, il che ovviamente causerebbe problemi se il compilatore dovesse "organizzare un altro comportamento" ( ad esempio, utilizzare istruzioni aggiuntive per verificare il potenziale trabocco e calcolare diversamente in quel caso).

Vale anche la pena notare che "comportamento indefinito" non significa "non funziona". Significa che l'implementazione è autorizzata a fare tutto ciò che gli piace in quella situazione. Ciò include fare "la cosa giusta" e "chiamare la polizia" o "schiantarsi". La maggior parte dei compilatori, quando possibile, sceglierà "fai la cosa giusta", supponendo che sia relativamente facile da definire (in questo caso, lo è). Tuttavia, se si verificano overflow nei calcoli, è importante capire cosa si traduce in realtà e che il compilatore PUO 'fare qualcosa di diverso da quello che ci si aspetta (e che ciò può dipendere molto dalla versione del compilatore, dalle impostazioni di ottimizzazione, ecc.) .


23
I compilatori non vogliono che tu faccia affidamento sul fatto che facciano la cosa giusta, e la maggior parte di loro te lo mostrerà non appena compili int f(int x) { return x+1>x; }con l'ottimizzazione. GCC e ICC, con le opzioni predefinite, ottimizzano quanto sopra return 1;.
Pascal Cuoq,

1
Per un programma di esempio che fornisce risultati diversi di fronte a un intoverflow a seconda dei livelli di ottimizzazione, vedi ideone.com/cki8nM Penso che ciò dimostri che la tua risposta dà cattivi consigli.
Magnus Hoff,

Ho modificato un po 'quella parte.
Mats Petersson,

Se una C dovesse fornire un mezzo per dichiarare un intero "complemento firmato due complementi", nessuna piattaforma in grado di eseguire C dovrebbe avere molti problemi a supportarla almeno in modo moderatamente efficiente. L'overhead aggiuntivo sarebbe sufficiente affinché il codice non utilizzi tale tipo quando non è richiesto il comportamento di wrapping, ma la maggior parte delle operazioni sugli interi di complemento a due sono identiche a quelle su interi senza segno, ad eccezione di confronti e promozioni.
supercat,

1
I valori negativi devono esistere e "funzionare" affinché il compilatore funzioni correttamente. Naturalmente è del tutto possibile aggirare la mancanza di valori con segno all'interno di un processore e utilizzare valori senza segno, sia come complementi che come complementi a due, a seconda di quale senso basato su ciò che l'insieme di istruzioni è. In genere sarebbe molto più lento farlo che avere il supporto hardware per esso, ma non è diverso dai processori che non supportano il virgola mobile nell'hardware o simili - aggiunge solo molto codice extra.
Mats Petersson,

10

Prima di tutto, tieni presente che C11 3.4.3, come tutti gli esempi e le note a piè di pagina, non è un testo normativo e quindi non è pertinente da citare!

Il testo rilevante che afferma che l'overflow di numeri interi e float è un comportamento indefinito è questo:

C11 6.5 / 5

Se si verifica una condizione eccezionale durante la valutazione di un'espressione (ovvero, se il risultato non è definito matematicamente o non è compreso nell'intervallo di valori rappresentabili per il suo tipo), il comportamento non è definito.

Un chiarimento riguardante il comportamento di tipi interi senza segno può essere trovato qui:

C11 6.2.5 / 9

L'intervallo di valori non negativi di un tipo intero con segno è una sottorange del tipo intero senza segno corrispondente e la rappresentazione dello stesso valore in ciascun tipo è la stessa. Un calcolo che coinvolge operandi senza segno non può mai traboccare, poiché un risultato che non può essere rappresentato dal tipo intero senza segno risultante viene ridotto del modulo che è uno maggiore del valore più grande che può essere rappresentato dal tipo risultante.

Ciò rende i tipi interi senza segno un caso speciale.

Si noti inoltre che esiste un'eccezione se qualsiasi tipo viene convertito in un tipo con segno e il vecchio valore non può più essere rappresentato. Il comportamento è quindi semplicemente definito dall'implementazione, sebbene possa essere emesso un segnale.

C11 6.3.1.3

6.3.1.3 Numeri interi firmati e non firmati

Quando un valore con tipo intero viene convertito in un altro tipo intero diverso da _Bool, se il valore può essere rappresentato dal nuovo tipo, rimane invariato.

Altrimenti, se il nuovo tipo non è firmato, il valore viene convertito aggiungendo o sottraendo ripetutamente uno in più rispetto al valore massimo che può essere rappresentato nel nuovo tipo fino a quando il valore non rientra nell'intervallo del nuovo tipo.

Altrimenti, il nuovo tipo è firmato e il valore non può essere rappresentato in esso; il risultato è definito dall'implementazione o viene generato un segnale definito dall'implementazione.


6

Oltre alle altre questioni menzionate, l'involucro matematico senza segno fa sì che i tipi interi senza segno si comportino come gruppi algebrici astratti (il che significa che, tra le altre cose, per qualsiasi coppia di valori Xe Y, esisterà qualche altro valore Ztale che X+Z, se correttamente cast , uguale ). Se i valori senza segno erano semplicemente tipi di posizione di archiviazione e non tipi di espressione intermedia (ad esempio se non esistevano equivalenti senza segno del tipo intero più grande e le operazioni aritmetiche sui tipi senza segno si comportavano come se fossero prima convertiti in tipi con segno più grande, quindi lì non sarebbe tanto necessario per un comportamento di avvolgimento definito, ma è difficile fare calcoli in un tipo che non ha, ad esempio, un inverso additivo.Y e Y-Zsarà, se correttamente lanciato, ugualeX

Questo aiuta in situazioni in cui il comportamento avvolgente è effettivamente utile, ad esempio con numeri di sequenza TCP o determinati algoritmi, come il calcolo dell'hash. Può anche essere utile nelle situazioni in cui è necessario rilevare l'overflow, poiché eseguire i calcoli e controllare se hanno traboccato è spesso più facile che verificare in anticipo se traboccerebbero, specialmente se i calcoli coinvolgono il tipo intero più grande disponibile.


Non seguo del tutto - perché aiuta avere un inverso additivo? Non riesco davvero a pensare a nessuna situazione in cui il comportamento di overflow sia effettivamente utile ...
sleske,

@sleske: l'utilizzo del decimale per la leggibilità umana, se un contatore di energia legge 0003 e la lettura precedente era 9995, significa che sono state utilizzate -9992 unità di energia o che sono state utilizzate 0008 unità di energia? Avere 0003-9995 resa 0008 semplifica il calcolo di quest'ultimo risultato. Avere il rendimento -9992 lo renderebbe un po 'più imbarazzante. Non essere in grado di farlo neanche, tuttavia, renderebbe necessario confrontare 0003 a 9995, notare che è inferiore, fare la sottrazione inversa, sottrarre quel risultato da 9999 e aggiungere 1.
supercat

@sleske: è anche molto utile sia per gli umani che per i compilatori essere in grado di applicare le leggi associative, distributive e commutative dell'aritmetica per riscrivere le espressioni e semplificarle; per esempio, se l'espressione a+b-cviene calcolata all'interno di un ciclo, ma be csono costanti all'interno di tale ciclo, può essere utile per spostare calcolo (b-c)all'esterno del ciclo, ma facendo che richiederebbe tra l'altro che (b-c)producono un valore che, quando inserito a, produrrà a+b-c, che a sua volta richiede che cabbia un inverso additivo.
supercat

: Grazie per le spiegazioni. Se lo capisco correttamente, tutti i tuoi esempi presumono che tu voglia gestire l'overflow. Nella maggior parte dei casi che ho riscontrato, l'overflow è indesiderabile e si desidera impedirlo, perché il risultato di un calcolo con overflow non è utile. Ad esempio, per il contatore di energia probabilmente si desidera utilizzare un tipo tale che non si verifichi mai un trabocco.
sleske

1
... tale che (a+b)-cequivale a a+(b-c)che il valore aritmetico di sia o meno b-crappresentabile all'interno del tipo, la sostituzione sarà valida indipendentemente dal possibile intervallo di valori per (b-c).
supercat

1

Forse un altro motivo per cui è definita l'aritmetica senza segno è perché i numeri senza segno formano numeri interi modulo 2 ^ n, dove n è la larghezza del numero senza segno. I numeri senza segno sono semplicemente numeri interi rappresentati utilizzando cifre binarie anziché cifre decimali. L'esecuzione delle operazioni standard in un sistema di moduli è ben compresa.

La citazione del PO si riferisce a questo fatto, ma evidenzia anche il fatto che esiste un solo modo logico e inequivocabile di rappresentare numeri interi senza segno in binario. Al contrario, i numeri firmati sono spesso rappresentati usando il complemento a due, ma sono possibili altre scelte come descritto nella norma (sezione 6.2.6.2).

La rappresentazione del complemento a due consente a determinate operazioni di avere più senso in formato binario. Ad esempio, aumentare i numeri negativi è lo stesso che per i numeri positivi (aspettarsi in condizioni di overflow). Alcune operazioni a livello di macchina possono essere le stesse per i numeri con segno e senza segno. Tuttavia, quando si interpretano i risultati di tali operazioni, alcuni casi non hanno senso: overflow positivo e negativo. Inoltre, i risultati di overflow differiscono a seconda della rappresentazione firmata sottostante.


Affinché una struttura sia un campo, ogni elemento della struttura diverso dall'identità additiva deve avere un inverso moltiplicativo. Una struttura di numeri interi congruenti mod N sarà un campo solo quando N è uno o primo [un campo degenerato quando N == 1]. C'è qualcosa che ritieni mi sia sfuggito nella mia risposta?
supercat

Hai ragione. Sono stato confuso dai moduli di potenza principali. Risposta originale modificata.
yth

Extra confusione qui è che ci sia un campo di ordine 2 ^ n, non è solo l'anello-isomorfo agli interi modulo 2 ^ n.
Kevin Ventullo,

E 2 ^ 31-1 è un Mersenne Prime (ma 2 ^ 63-1 non è un numero primo). Pertanto, la mia idea originale è stata rovinata. Inoltre, le dimensioni intere erano diverse nel corso della giornata. Quindi, la mia idea era revisionista nella migliore delle ipotesi.
yth

Il fatto che numeri interi senza segno formino un anello (non un campo), prendendo anche la parte di ordine inferiore produce un anello, e l'esecuzione di operazioni sull'intero valore e quindi il troncamento si comporteranno equivalenti all'esecuzione delle operazioni sulla parte inferiore, erano IMHO quasi certamente considerazioni.
supercat
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.