(A + B + C) ≠ (A + C + B) e riordino del compilatore


108

L'aggiunta di due interi a 32 bit può causare un overflow di numeri interi:

uint64_t u64_z = u32_x + u32_y;

Questo overflow può essere evitato se uno degli interi a 32 bit viene prima castato o aggiunto a un intero a 64 bit.

uint64_t u64_z = u32_x + u64_a + u32_y;

Tuttavia, se il compilatore decide di riordinare l'aggiunta:

uint64_t u64_z = u32_x + u32_y + u64_a;

il numero intero overflow potrebbe ancora verificarsi.

I compilatori sono autorizzati a fare un tale riordino o possiamo fidarci di loro per notare l'incoerenza del risultato e mantenere l'ordine delle espressioni così com'è?


15
In realtà non mostri l'overflow di numeri interi perché sembri essere uint32_tvalori aggiunti , che non overflow, vanno a capo. Questi non sono comportamenti diversi.
Martin Bonner supporta Monica il

5
Vedi la sezione 1.9 degli standard c ++, risponde direttamente alla tua domanda (c'è anche un esempio che è quasi esattamente uguale al tuo).
Holt

3
@Tal: come altri hanno già affermato: non c'è overflow di numeri interi. Unsigned è definito per il wrapping, poiché firmato è un comportamento non definito, quindi qualsiasi implementazione andrà bene, inclusi i daemon nasali.
troppo onesto per questo sito

5
@Tal: sciocchezze! Come ho già scritto: lo standard è molto chiaro e richiede avvolgimento, non saturazione (ciò sarebbe possibile con firmato, in quanto è UB come standard.
troppo onesto per questo sito

15
@rustyx: che tu lo chiami wrapping o overflow, il punto rimane che si ((uint32_t)-1 + (uint32_t)1) + (uint64_t)0traduce in 0, mentre si (uint32_t)-1 + ((uint32_t)1 + (uint64_t)0)traduce in 0x100000000, e questi due valori non sono uguali. Quindi è significativo se il compilatore può applicare o meno tale trasformazione. Ma sì, lo standard usa solo la parola "overflow" per interi con segno, non senza segno.
Steve Jessop

Risposte:


84

Se l'ottimizzatore esegue un tale riordino, è ancora vincolato alla specifica C, quindi un tale riordino diventerebbe:

uint64_t u64_z = (uint64_t)u32_x + (uint64_t)u32_y + u64_a;

Fondamento logico:

Cominciamo con

uint64_t u64_z = u32_x + u64_a + u32_y;

L'aggiunta viene eseguita da sinistra a destra.

Le regole di promozione intera stabiliscono che nella prima aggiunta nell'espressione originale, u32_xessere promosso a uint64_t. Nella seconda aggiunta, u32_ysarà anche promosso auint64_t .

Quindi, per essere conforme alla specifica C, qualsiasi ottimizzatore deve promuovere u32_xe u32_yvalori senza segno a 64 bit. Ciò equivale ad aggiungere un cast. (L'ottimizzazione effettiva non viene eseguita a livello C, ma io uso la notazione C perché è una notazione che comprendiamo.)


Non è associativo di sinistra, quindi (u32_x + u32_t) + u64_a?
Inutile

12
@ Inutile: Klas ha trasmesso tutto a 64 bit. Ora l'ordine non fa alcuna differenza. Il compilatore non ha bisogno di seguire l'associatività, deve solo produrre lo stesso identico risultato come se lo facesse.
gnasher729

2
Sembra suggerire che il codice di OP verrebbe valutato in questo modo, il che non è vero.
Inutile

@Klas - ti interessa spiegare perché è così e come arrivi esattamente al tuo esempio di codice?
rustyx

1
@rustyx Aveva bisogno di una spiegazione. Grazie per avermi spinto ad aggiungerne uno.
Klas Lindbäck

28

Un compilatore può riordinare solo in regola come if . Cioè, se il riordino darà sempre lo stesso risultato dell'ordine specificato, allora è consentito. Altrimenti (come nel tuo esempio), no.

Ad esempio, data la seguente espressione

i32big1 - i32big2 + i32small

che è stato accuratamente costruito per sottrarre i due valori noti per essere grandi ma simili, e solo allora aggiungere l'altro valore piccolo (evitando così qualsiasi overflow), il compilatore può scegliere di riordinare in:

(i32small - i32big2) + i32big1

e fare affidamento sul fatto che la piattaforma di destinazione utilizza l'aritmetica a due complementi con wrap-round per evitare problemi. (Un tale riordino potrebbe essere sensato se il compilatore viene premuto per i registri e capita di avere i32smallgià in un registro).


L'esempio di OP utilizza tipi non firmati. i32big1 - i32big2 + i32smallimplica tipi con segno. Entrano in gioco ulteriori preoccupazioni.
chux - Ripristina Monica il

@chux Assolutamente. Il punto che stavo cercando di sottolineare è che, sebbene non potessi scrivere (i32small-i32big2) + i32big1, (perché potrebbe causare UB), il compilatore può riorganizzarlo in modo efficace perché il compilatore può essere sicuro che il comportamento sarà corretto.
Martin Bonner supporta Monica il

3
@chux: Ulteriori preoccupazioni come UB non entrano in gioco, perché stiamo parlando di un compilatore che riordina secondo la regola come-if. Un particolare compilatore può trarre vantaggio dalla conoscenza del proprio comportamento di overflow.
MSalters

16

Esiste la regola "come se" in C, C ++ e Objective-C: il compilatore può fare tutto ciò che vuole fintanto che nessun programma conforme può distinguere.

In queste lingue, a + b + c è definito come lo stesso di (a + b) + c. Se riesci a capire la differenza tra questo e ad esempio a + (b + c), il compilatore non può modificare l'ordine. Se non puoi dire la differenza, allora il compilatore è libero di cambiare l'ordine, ma va bene, perché non puoi dire la differenza.

Nel tuo esempio, con b = 64 bit, aec 32 bit, il compilatore sarebbe autorizzato a valutare (b + a) + c o anche (b + c) + a, perché non potresti dire la differenza, ma non (a + c) + b perché puoi vedere la differenza.

In altre parole, al compilatore non è consentito fare nulla che renda il tuo codice diverso da quello che dovrebbe. Non è necessario produrre il codice che si pensa possa produrre, o che si pensa debba produrre, ma il codice vi darà esattamente i risultati che dovrebbe.


Ma con un grande avvertimento; il compilatore è libero di non assumere alcun comportamento indefinito (in questo caso overflow). Questo è simile a come if (a + 1 < a)può essere ottimizzato un controllo di overflow .
csiz

7
@csiz ... su firmati variabili. Le variabili senza segno hanno una semantica di overflow ben definita (wrap-around).
Gavin S. Yancey

7

Citando dagli standard :

[Nota: gli operatori possono essere raggruppati secondo le solite regole matematiche solo dove gli operatori sono realmente associativi o commutativi.7 Ad esempio, nel seguente frammento int a, b;

/∗ ... ∗/
a = a + 32760 + b + 5;

l'istruzione dell'espressione si comporta esattamente come

a = (((a + 32760) + b) + 5);

per l'associatività e la precedenza di questi operatori. Pertanto, il risultato della somma (a + 32760) viene successivamente aggiunto a b, e quel risultato viene quindi aggiunto a 5 che risulta nel valore assegnato a a. Su una macchina in cui gli overflow producono un'eccezione e in cui l'intervallo di valori rappresentabili da un int è [-32768, + 32767], l'implementazione non può riscrivere questa espressione come

a = ((a + b) + 32765);

poiché se i valori per aeb fossero, rispettivamente, -32754 e -15, la somma a + b produrrebbe un'eccezione mentre l'espressione originale no; né l'espressione può essere riscritta come

a = ((a + 32765) + b);

o

a = (a + (b + 32765));

poiché i valori per aeb potrebbero essere stati, rispettivamente, 4 e -8 o -17 e 12. Tuttavia su una macchina in cui gli overflow non producono un'eccezione e in cui i risultati degli overflow sono reversibili, l'istruzione di espressione sopra può essere riscritto dall'implementazione in uno dei modi precedenti perché si verificherà lo stesso risultato. - nota finale]


4

I compilatori sono autorizzati a fare un tale riordino o possiamo fidarci di loro per notare l'incoerenza del risultato e mantenere l'ordine delle espressioni così com'è?

Il compilatore può riordinare solo se fornisce lo stesso risultato - qui, come hai osservato, non funziona.


È possibile scrivere un modello di funzione, se ne vuoi uno, che promuove tutti gli argomenti std::common_typeprima dell'aggiunta: questo sarebbe sicuro e non fare affidamento sull'ordine degli argomenti o sul casting manuale, ma è piuttosto goffo.


So che dovrebbe essere usato il casting esplicito, ma desidero conoscere il comportamento dei compilatori quando tale casting è stato erroneamente omesso.
Tal

1
Come ho detto, senza casting esplicito: viene eseguita per prima l'aggiunta sinistra, senza promozione integrale, e quindi soggetta a wrapping. Il risultato di tale aggiunta, eventualmente racchiuso, viene quindi promosso a uint64_tper l'aggiunta al valore più a destra.
Inutile

La tua spiegazione sulla regola come se fosse totalmente sbagliata. Il linguaggio C, ad esempio, specifica quali operazioni dovrebbero essere eseguite su una macchina astratta. La regola del "come se" gli consente di fare assolutamente quello che vuole finché nessuno può dire la differenza.
gnasher729

Il che significa che il compilatore può fare quello che vuole purché il risultato sia lo stesso di quello determinato dalle regole di associatività a sinistra e di conversione aritmetica mostrate.
Inutile

1

Dipende dalla larghezza di bit di unsigned/int.

I 2 sotto non sono gli stessi (quando unsigned <= 32bit). u32_x + u32_ydiventa 0.

u64_a = 0; u32_x = 1; u32_y = 0xFFFFFFFF;
uint64_t u64_z = u32_x + u64_a + u32_y;
uint64_t u64_z = u32_x + u32_y + u64_a;  // u32_x + u32_y carry does not add to sum.

Sono gli stessi (quando unsigned >= 34bit). Promozioni intere causate u32_x + u32_y aggiunta a 64 bit di matematica. L'ordine è irrilevante.

È UB (quando unsigned == 33bit). Le promozioni intere hanno causato l'aggiunta in matematica a 33 bit con segno e l'overflow con segno è UB.

I compilatori sono autorizzati a fare un tale riordino ...?

(Matematica a 32 bit): Riordinare sì, ma devono verificarsi gli stessi risultati, quindi non che il riordino OP propone. Di seguito sono gli stessi

// Same
u32_x + u64_a + u32_y;
u64_a + u32_x + u32_y;
u32_x + (uint64_t) u32_y + u64_a;
...

// Same as each other below, but not the same as the 3 above.
uint64_t u64_z = u32_x + u32_y + u64_a;
uint64_t u64_z = u64_a + (u32_x + u32_y);

... possiamo fidarci di loro per notare l'incoerenza del risultato e mantenere l'ordine delle espressioni così com'è?

Fidati di sì, ma l'obiettivo di codifica di OP non è del tutto chiaro. Il u32_x + u32_ycarry dovrebbe contribuire? Se OP vuole quel contributo, il codice dovrebbe essere

uint64_t u64_z = u64_a + u32_x + u32_y;
uint64_t u64_z = u32_x + u64_a + u32_y;
uint64_t u64_z = u32_x + (u32_y + u64_a);

Ma no

uint64_t u64_z = u32_x + u32_y + u64_a;
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.