((A + (b & 255)) & 255) è uguale a ((a + b) & 255)?


92

Stavo esplorando un po 'di codice C ++ e ho trovato qualcosa del genere:

(a + (b & 255)) & 255

Il doppio AND mi ha infastidito, quindi ho pensato a:

(a + b) & 255

( ae bsono interi senza segno a 32 bit)

Ho scritto rapidamente uno script di test (JS) per confermare la mia teoria:

for (var i = 0; i < 100; i++) {
    var a = Math.ceil(Math.random() * 0xFFFF),
        b = Math.ceil(Math.random() * 0xFFFF);

    var expr1 = (a + (b & 255)) & 255,
        expr2 = (a + b) & 255;

    if (expr1 != expr2) {
        console.log("Numbers " + a + " and " + b + " mismatch!");
        break;
    }
}

Anche se lo script ha confermato la mia ipotesi (entrambe le operazioni sono uguali), non mi fido ancora, perché 1) casuale e 2) non sono un matematico, non ho idea di cosa sto facendo .

Inoltre, ci scusiamo per il titolo Lisp-y. Sentiti libero di modificarlo.


4
In che lingua è quella sceneggiatura? Non Math.random()restituire un numero intero o una doppia [0,1)? Non credo che la tua sceneggiatura (la cosa migliore che posso dire) rifletta il problema che hai posto.
Brick

7
Cos'è il codice c / c ++? Sono lingue diverse.
Banderuola

14
Non puoi riprodurre il comportamento che stai tentando di testare in JS. Ecco perché tutti sono solo tu per la scelta della lingua. JS non è fortemente tipizzato e la risposta dipende in modo critico dal tipo di variabili in C / C ++. Il JS è una totale assurdità data la domanda che hai posto.
Brick

4
@WeatherVane Questo è essenzialmente pseudo-codice, utilizzando i nomi delle funzioni Javascript. La sua domanda riguarda il comportamento di &e +su interi senza segno in C e C ++.
Barmar

11
Tieni presente che "Ho scritto un programma di test e ho ottenuto la risposta che mi aspettavo per tutti i possibili input" non è in realtà una garanzia che qualcosa si comporti come ti aspetti. Un comportamento indefinito può essere così sgradevole; dando risultati inaspettati solo dopo aver finito di convincerti che il tuo codice è corretto.

Risposte:


78

Loro sono la stessa cosa. Ecco una prova:

Prima nota l'identità (A + B) mod C = (A mod C + B mod C) mod C

Riaffermiamo il problema considerandolo a & 255come sostituto a % 256. Questo è vero poiché anon è firmato.

Così (a + (b & 255)) & 255è(a + (b % 256)) % 256

Questo è lo stesso di (a % 256 + b % 256 % 256) % 256(ho applicato l'identità sopra indicata: nota che mode %sono equivalenti per i tipi non firmati.)

Questo semplifica a (a % 256 + b % 256) % 256quale diventa (a + b) % 256(riapplicare l'identità). Puoi quindi rimettere l'operatore bit a bit per dare

(a + b) & 255

completare la dimostrazione.


81
È una prova matematica, ignorando la possibilità di overflow. Considera A=0xFFFFFFFF, B=1, C=3. La prima identità non regge. (L'overflow non sarà un problema per l'aritmetica senza segno, ma è una cosa leggermente diversa.)
AlexD

4
In realtà, (a + (b & 255)) & 255è uguale a (a + (b % 256)) % N % 256, dove Nè uno maggiore del valore massimo senza segno. (quest'ultima formula deve essere interpretata come aritmetica di numeri interi matematici)

17
Dimostrazioni matematiche come questa non sono appropriate per dimostrare il comportamento degli interi sulle architetture dei computer.
Jack Aidley

25
@JackAidley: sono appropriati se eseguiti correttamente (il che non lo è, a causa del fatto che si trascura di considerare l'overflow).

3
@ Shaz: Questo è vero per lo script di test, ma non fa parte della domanda posta.

21

In addizione posizionale, sottrazione e moltiplicazione di numeri senza segno per produrre risultati senza segno, le cifre più significative dell'input non influenzano le cifre meno significative del risultato. Questo si applica all'aritmetica binaria tanto quanto all'aritmetica decimale. Si applica anche all'aritmetica con segno di "complemento a due", ma non all'aritmetica con segno di grandezza del segno.

Tuttavia dobbiamo stare attenti quando prendiamo le regole dall'aritmetica binaria e le applichiamo a C (credo che C ++ abbia le stesse regole di C su questa roba ma non ne sono sicuro al 100%) perché l'aritmetica C ha alcune regole arcane che possono farci inciampare su. L'aritmetica senza segno in C segue semplici regole di avvolgimento binarie ma l'overflow aritmetico con segno è un comportamento indefinito. Peggio ancora, in alcune circostanze C "promuoverà" automaticamente un tipo senza segno a (firmato) int.

Il comportamento indefinito in C può essere particolarmente insiduo. È probabile che un compilatore stupido (o un compilatore con un livello di ottimizzazione basso) faccia ciò che ti aspetti in base alla tua comprensione dell'aritmetica binaria mentre un compilatore ottimizzato potrebbe rompere il tuo codice in modi strani.


Quindi, tornando alla formula nella domanda, l'equivalenza dipende dai tipi di operando.

Se sono interi senza segno la cui dimensione è maggiore o uguale alla dimensione di, intil comportamento di overflow dell'operatore di addizione è ben definito come semplice avvolgimento binario. Il fatto che mascheriamo o meno i 24 bit alti di un operando prima dell'operazione di addizione non ha alcun impatto sui bit bassi del risultato.

Se sono numeri interi senza segno la cui dimensione è minore di intallora verranno promossi a (con segno ) int. L'overflow di interi con segno è un comportamento indefinito, ma almeno su ogni piattaforma ho riscontrato che la differenza di dimensioni tra diversi tipi di interi è abbastanza grande che una singola aggiunta di due valori promossi non causerà overflow. Quindi di nuovo possiamo ricorrere all'argomento aritmetico semplicemente binario per ritenere le istruzioni equivalenti.

Se sono numeri interi con segno la cui dimensione è inferiore a int, di nuovo l'overflow non può verificarsi e su implementazioni a complemento a due possiamo fare affidamento sull'argomento aritmetico binario standard per dire che sono equivalenti. Sulla grandezza dei segni o sulle implementazioni complementari non sarebbero equivalenti.

OTOH se ae bfossero numeri interi con segno la cui dimensione fosse maggiore o uguale alla dimensione di int allora anche su implementazioni a complemento a due ci sono casi in cui un'istruzione sarebbe ben definita mentre l'altra sarebbe un comportamento indefinito.


20

Lemma: a & 255 == a % 256per non firmato a.

Unsigned apuò essere riscritta come m * 0x100 + balcuni non firmato m, b, 0 <= b < 0xff, 0 <= m <= 0xffffff. Ne consegue da entrambe le definizioni che a & 255 == b == a % 256.

Inoltre, abbiamo bisogno di:

  • la proprietà distributiva: (a + b) mod n = [(a mod n) + (b mod n)] mod n
  • la definizione di addizione senza segno, matematicamente: (a + b) ==> (a + b) % (2 ^ 32)

Quindi:

(a + (b & 255)) & 255 = ((a + (b & 255)) % (2^32)) & 255      // def'n of addition
                      = ((a + (b % 256)) % (2^32)) % 256      // lemma
                      = (a + (b % 256)) % 256                 // because 256 divides (2^32)
                      = ((a % 256) + (b % 256 % 256)) % 256   // Distributive
                      = ((a % 256) + (b % 256)) % 256         // a mod n mod n = a mod n
                      = (a + b) % 256                         // Distributive again
                      = (a + b) & 255                         // lemma

Quindi sì, è vero. Per interi senza segno a 32 bit.


E gli altri tipi di numeri interi?

  • Per 64 bit interi senza segno, tutto quanto sopra si applica altrettanto bene, semplicemente sostituendo 2^64per 2^32.
  • Per interi senza segno a 8 e 16 bit, l'aggiunta implica la promozione a int. Questo intsicuramente non sarà né traboccante né negativo in nessuna di queste operazioni, quindi rimangono tutte valide.
  • Per firmati interi, se uno a+bo a+(b&255)troppo pieno, è un comportamento indefinito. Quindi l'uguaglianza non può reggere - ci sono casi in cui(a+b)&255 è un comportamento indefinito ma (a+(b&255))&255non lo è.

17

Sì, (a + b) & 255 va bene.

Ricordi l'aggiunta a scuola? Aggiungi numeri cifra per cifra e aggiungi un valore di trasporto alla colonna di cifre successiva. Non è possibile che una colonna di cifre successiva (più significativa) influenzi una colonna già elaborata. Per questo motivo, non fa differenza se azzerare le cifre solo nel risultato o anche per prime in un argomento.


Quanto sopra non è sempre vero, lo standard C ++ consente un'implementazione che lo romperebbe.

Tale Deathstation 9000 : - ) dovrebbe usare un 33 bit int, se l'OP intendesse unsigned shortcon "interi senza segno a 32 bit". Se unsigned intsi intendeva, il DS9K dovrebbe usare un 32 bit inte un 32 bit unsigned intcon un bit di riempimento. (Gli interi senza segno devono avere la stessa dimensione delle loro controparti con segno come da §3.9.1 / 3, e i bit di riempimento sono consentiti in §3.9.1 / 1.) Anche altre combinazioni di dimensioni e bit di riempimento funzionerebbero.

Per quanto ne so, questo è l'unico modo per romperlo, perché:

  • La rappresentazione intera deve usare uno schema di codifica "puramente binario" (§3.9.1 / 7 e la nota a piè di pagina), tutti i bit tranne i bit di riempimento e il bit di segno devono contribuire con un valore di 2 n
  • La promozione int è consentita solo se intpuò rappresentare tutti i valori del tipo sorgente (§4.5 / 1), quindi intdeve avere almeno 32 bit che contribuiscono al valore, più un bit di segno.
  • il intnon può avere più bit di valore (senza contare il bit di segno) di 32, perché altrimenti un'addizione non può overflow.

2
Ci sono molte altre operazioni oltre all'aggiunta in cui la spazzatura nei bit alti non influenza il risultato nei bit bassi che ti interessano. Vedi questa domanda e risposta sul complemento a 2 , che usa x86 asm come caso d'uso, ma si applica anche a interi binari senza segno in qualsiasi situazione.
Peter Cordes

2
Sebbene sia ovviamente diritto di tutti votare in modo anonimo, apprezzo sempre un commento come un'opportunità per imparare.
alain

2
Questa è di gran lunga la risposta / argomento più semplice da capire, IMO. Il trasferimento / prestito in aggiunta / sottrazione si propaga solo da bit bassi a bit alti (da destra a sinistra) in binario, come in decimale. IDK perché qualcuno dovrebbe downvote questo.
Peter Cordes

1
@Bathsheba: CHAR_BIT non deve essere 8. Ma i tipi senza segno in C e C ++ devono comportarsi come normali interi binari base2 di una certa larghezza di bit. Penso che ciò richieda che UINT_MAX sia 2^N-1. (N potrebbe non essere nemmeno richiesto di essere un multiplo di CHAR_BIT, dimentico, ma sono abbastanza sicuro che lo standard richiede che il wraparound avvenga con una potenza di 2.) Penso che l'unico modo per ottenere la stranezza sia tramite la promozione a un tipo firmato abbastanza largo da contenere ao bma non abbastanza largo da contenere a+bin tutti i casi.
Peter Cordes

2
@Bathsheba: sì, fortunatamente C-as-portable-assembly-language funziona davvero principalmente per i tipi non firmati. Nemmeno un'implementazione C intenzionalmente ostile può rompere questo. Sono solo i tipi con firma in cui le cose sono orribili per bit-hack veramente portatili in C, e una Deathstation 9000 può davvero rompere il tuo codice.
Peter Cordes

14

Hai già la risposta intelligente: l'aritmetica senza segno è modulo aritmetica e quindi i risultati manterranno, puoi dimostrarlo matematicamente ...


Una cosa interessante dei computer, tuttavia, è che i computer sono veloci. In effetti, sono così veloci che è possibile enumerare tutte le combinazioni valide di 32 bit in un ragionevole lasso di tempo (non provare con 64 bit).

Quindi, nel tuo caso, personalmente mi piace semplicemente lanciarlo su un computer; mi ci vuole meno tempo per convincermi che il programma è corretto di quanto ci vuole per convincermi che la dimostrazione matematica sia corretta e che non ho supervisionato un dettaglio nella specifica 1 :

#include <iostream>
#include <limits>

int main() {
    std::uint64_t const MAX = std::uint64_t(1) << 32;
    for (std::uint64_t i = 0; i < MAX; ++i) {
        for (std::uint64_t j = 0; j < MAX; ++j) {
            std::uint32_t const a = static_cast<std::uint32_t>(i);
            std::uint32_t const b = static_cast<std::uint32_t>(j);

            auto const champion = (a + (b & 255)) & 255;
            auto const challenger = (a + b) & 255;

            if (champion == challenger) { continue; }

            std::cout << "a: " << a << ", b: " << b << ", champion: " << champion << ", challenger: " << challenger << "\n";
            return 1;
        }
    }

    std::cout << "Equality holds\n";
    return 0;
}

Questo enumera tutti i possibili valori di ae bnello spazio a 32 bit e controlla se l'uguaglianza è valida o meno. In caso contrario, stampa il caso che non ha funzionato, che puoi utilizzare come controllo di integrità.

E, secondo Clang : l' uguaglianza vale .

Inoltre, dato che le regole aritmetiche sono indipendenti dalla larghezza di bit (sopra la intlarghezza di bit), questa uguaglianza sarà valida per qualsiasi tipo di intero senza segno di 32 bit o più, inclusi 64 bit e 128 bit.

Nota: come può un compilatore enumera tutti i modelli a 64 bit in un lasso di tempo ragionevole? Non può. I loop sono stati ottimizzati. Altrimenti saremmo morti tutti prima che l'esecuzione terminasse.


Inizialmente l'ho provato solo per interi senza segno a 16 bit; sfortunatamente C ++ è un linguaggio folle in cui intvengono prima convertiti piccoli interi (larghezze di bit più piccole di ) int.

#include <iostream>

int main() {
    unsigned const MAX = 65536;
    for (unsigned i = 0; i < MAX; ++i) {
        for (unsigned j = 0; j < MAX; ++j) {
            std::uint16_t const a = static_cast<std::uint16_t>(i);
            std::uint16_t const b = static_cast<std::uint16_t>(j);

            auto const champion = (a + (b & 255)) & 255;
            auto const challenger = (a + b) & 255;

            if (champion == challenger) { continue; }

            std::cout << "a: " << a << ", b: " << b << ", champion: "
                      << champion << ", challenger: " << challenger << "\n";
            return 1;
        }
    }

    std::cout << "Equality holds\n";
    return 0;
}

E ancora una volta, secondo Clang : l' uguaglianza vale .

Bene, eccoti :)


1 Naturalmente, se un programma inavvertitamente innesca un comportamento indefinito, non si dimostrerebbe molto.


1
dici che è facile da fare con valori a 32 bit ma in realtà usi 16 bit ...: D
Willi Mentzel

1
@WilliMentzel: Questa è un'osservazione interessante. Inizialmente volevo dire che se funziona con 16 bit, funzionerà allo stesso modo con 32 bit, 64 bit e 128 bit perché lo Standard non ha un comportamento specifico per diverse larghezze di bit ... tuttavia mi sono ricordato che in realtà lo fa per larghezze di bit inferiori a quella di int: i piccoli numeri interi vengono prima convertiti in int(una regola strana). Quindi devo effettivamente fare la dimostrazione con 32 bit (e successivamente si estende a 64 bit, 128 bit, ...).
Matthieu M.

2
Dal momento che non puoi valutare tutti (4294967296 - 1) * (4294967296 - 1) i possibili risultati, riduci in qualche modo? Secondo me MAX dovrebbe essere (4294967296 - 1) se vai da quella parte ma non finirà mai nella nostra vita come hai detto tu ... quindi, dopotutto non possiamo mostrare l'uguaglianza in un esperimento, almeno non in uno come te descrivere.
Willi Mentzel,

1
Testare questo sull'implementazione del complemento di uno 2 non dimostra che sia portabile per la grandezza del segno o il complemento a uno con le larghezze di tipo Deathstation 9000. ad esempio, un tipo stretto senza segno potrebbe passare a un 17 bit intche può rappresentare ogni possibile uint16_t, ma dove a+bpuò overflow. Questo è un problema solo per i tipi senza segno più stretti di int; C richiede che i unsignedtipi siano interi binari, quindi l'avvolgimento avviene modulo una potenza di 2
Peter Cordes

1
Concordava sul fatto che C fosse troppo portabile per il suo bene. Sarebbe davvero bello se standardizzassero il complemento di 2, gli spostamenti aritmetici a destra per il segno e un modo per eseguire l'aritmetica con segno con semantica di avvolgimento invece di semantica a comportamento indefinito, per quei casi in cui si desidera il wrapping. Quindi il C potrebbe tornare utile come assemblatore portatile, invece che come campo minato grazie ai moderni compilatori ottimizzanti che rendono pericoloso lasciare qualsiasi comportamento indefinito (almeno per la tua piattaforma di destinazione. Il comportamento indefinito solo sulle implementazioni di Deathstation 9000 va bene indicare).
Peter Cordes

4

La risposta rapida è: entrambe le espressioni sono equivalenti

  • poiché ae bsono interi senza segno a 32 bit, il risultato è lo stesso anche in caso di overflow. l'aritmetica senza segno lo garantisce: un risultato che non può essere rappresentato dal tipo intero senza segno risultante viene ridotto modulo il numero che è maggiore di uno del valore più grande che può essere rappresentato dal tipo risultante.

La risposta lunga è: non esistono piattaforme note in cui queste espressioni differirebbero, ma lo Standard non lo garantisce, a causa delle regole di promozione integrale.

  • Se il tipo di ae b(interi senza segno a 32 bit) ha un rango più alto di int, il calcolo viene eseguito come senza segno, modulo 2 32 , e restituisce lo stesso risultato definito per entrambe le espressioni per tutti i valori di ae b.

  • Al contrario, se il tipo di ae bè minore di int, entrambi vengono promossi a inte il calcolo viene eseguito utilizzando l'aritmetica con segno, dove overflow richiama un comportamento non definito.

    • Se intha almeno 33 bit di valore, nessuna delle espressioni precedenti può overflow, quindi il risultato è perfettamente definito e ha lo stesso valore per entrambe le espressioni.

    • Se intha esattamente 32 bit di valore, il calcolo può overflow per entrambe le espressioni, ad esempio i valori, a=0xFFFFFFFFe b=1causerebbe un overflow in entrambe le espressioni. Per evitare ciò, dovresti scrivere ((a & 255) + (b & 255)) & 255.

  • La buona notizia è che non esistono tali piattaforme 1 .


1 Più precisamente, non esiste una piattaforma reale di questo tipo, ma è possibile configurare un DS9K in modo che mostri tale comportamento e sia comunque conforme allo standard C.


3
Il tuo secondo sottoblocco richiede che (1) asia minore di int(2) intabbia 32 bit di valore (3) a=0xFFFFFFFF. Non possono essere tutte vere.
Barry

1
@Barry: L'unico caso che sembra soddisfare i requisiti è 33 bit int, dove ci sono 32 bit di valore e un bit di segno.
Ben Voigt

2

Identico presumendo nessun trabocco . Nessuna delle due versioni è veramente immune al trabocco, ma la versione double e è più resistente ad essa. Non sono a conoscenza di un sistema in cui un overflow in questo caso è un problema, ma posso vedere l'autore che lo fa nel caso in cui ce ne sia uno.


1
L'OP specificato: (aeb sono interi senza segno a 32 bit) . A meno che non intsia largo 33 bit, il risultato è lo stesso anche in caso di overflow. l'aritmetica senza segno lo garantisce: un risultato che non può essere rappresentato dal tipo intero senza segno risultante viene ridotto modulo il numero che è maggiore di uno rispetto al valore più grande che può essere rappresentato dal tipo risultante.
chqrlie

2

Sì, puoi dimostrarlo con l'aritmetica, ma c'è una risposta più intuitiva.

Quando si aggiunge, ogni bit influenza solo quelli più significativi di se stesso; mai quelli meno significativi.

Pertanto, qualunque cosa tu faccia ai bit più alti prima dell'aggiunta non cambierà il risultato, a patto che manterrai solo i bit meno significativi del bit più basso modificato.


0

La dimostrazione è banale e lasciata come esercizio per il lettore

Ma per legittimare effettivamente questo come una risposta, la tua prima riga di codice dice che prendi gli ultimi 8 bit di b** (tutti i bit superiori bimpostati a zero) e aggiungilo aa quindi prendi solo gli ultimi 8 bit dell'impostazione del risultato tutti più alti bit a zero.

La seconda riga dice aggiungi aeb prendi gli ultimi 8 bit con tutti i bit superiori zero.

Solo gli ultimi 8 bit sono significativi nel risultato. Pertanto solo gli ultimi 8 bit sono significativi negli ingressi.

** ultimi 8 bit = 8 LSB

Inoltre è interessante notare che l'output sarebbe equivalente a

char a = something;
char b = something;
return (unsigned int)(a + b);

Come sopra, solo gli 8 LSB sono significativi, ma il risultato è un unsigned intcon tutti gli altri bit zero. Il a + btrabocco, producendo il risultato atteso.


No, non lo sarebbe. La matematica dei caratteri avviene come int e char potrebbe essere firmato.
Antti Haapala
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.