Apakah ((a + (b & 255)) & 255) sama dengan ((a + b) & 255)?

92

Saya sedang menjelajahi beberapa kode C ++, dan menemukan sesuatu seperti ini:

(a + (b & 255)) & 255

Ganda DAN mengganggu saya, jadi saya memikirkan:

(a + b) & 255

( adan bmerupakan bilangan bulat 32-bit unsigned)

Saya dengan cepat menulis skrip pengujian (JS) untuk mengkonfirmasi teori saya:

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;
    }
}

Sementara skrip mengkonfirmasi hipotesis saya (kedua operasi sama), saya masih tidak mempercayainya, karena 1) acak dan 2) Saya bukan ahli matematika, saya tidak tahu apa yang saya lakukan .

Juga, maaf untuk judul Lisp-y. Jangan ragu untuk mengeditnya.

Martin
sumber
4
Bahasa apa naskah itu? Apakah Math.random()mengembalikan integer atau double pada [0,1)? Saya tidak berpikir naskah Anda (sebaik yang saya tahu) mencerminkan masalah yang Anda ajukan sama sekali.
Bata
7
Apa itu kode c / c ++? Mereka adalah bahasa yang berbeda.
Weather Vane
14
Anda tidak dapat mereproduksi perilaku yang Anda coba uji di JS. Itu sebabnya semua orang hanya Anda tentang pilihan bahasa. JS tidak diketik dengan kuat dan jawabannya sangat bergantung pada jenis variabel di C / C ++. JS benar-benar tidak masuk akal mengingat pertanyaan yang Anda ajukan.
Bata
4
@WeatherVane Itu pseudo-code esensial, menggunakan nama fungsi Javascript. Pertanyaannya adalah tentang perilaku &dan +pada unsigned integers di C dan C ++.
Barmar
11
Perlu diingat bahwa "Saya menulis program pengujian dan mendapatkan jawaban yang saya harapkan untuk semua masukan yang mungkin" sebenarnya bukan jaminan bahwa sesuatu akan berperilaku seperti yang Anda harapkan. Perilaku yang tidak terdefinisi bisa menjadi jahat seperti itu; hanya memberikan hasil yang tidak diharapkan setelah Anda selesai meyakinkan diri sendiri bahwa kode Anda benar.

Jawaban:

78

Mereka sama. Ini buktinya:

Catat dulu identitasnya (A + B) mod C = (A mod C + B mod C) mod C

Mari kita ulangi masalah dengan menganggap a & 255sebagai berdiri untuk a % 256. Ini benar karena atidak ditandatangani.

Begitu (a + (b & 255)) & 255juga(a + (b % 256)) % 256

Ini sama dengan (a % 256 + b % 256 % 256) % 256(Saya telah menerapkan identitas yang disebutkan di atas: perhatikan itu moddan %setara untuk tipe yang tidak bertanda tangan.)

Ini menyederhanakan (a % 256 + b % 256) % 256menjadi (a + b) % 256(menerapkan kembali identitas). Anda kemudian dapat mengembalikan operator bitwise untuk memberi

(a + b) & 255

melengkapi buktinya.

Batsyeba
sumber
81
Ini adalah bukti matematis, mengabaikan kemungkinan melimpah. Pertimbangkan A=0xFFFFFFFF, B=1, C=3. Identitas pertama tidak berlaku. (Overflow tidak akan menjadi masalah untuk aritmatika unsigned, tetapi ini adalah hal yang sedikit berbeda.)
AlexD
4
Sebenarnya (a + (b & 255)) & 255sama saja dengan (a + (b % 256)) % N % 256, dimana Nsatu lebih besar dari nilai unsigned maksimum. (rumus terakhir dimaksudkan untuk ditafsirkan sebagai aritmatika bilangan bulat matematika)
17
Bukti matematis seperti ini tidak sesuai untuk membuktikan perilaku bilangan bulat pada arsitektur komputer.
Jack Aidley
25
@JackAidley: Mereka sesuai bila dilakukan dengan benar (yang mana yang salah, karena lalai mempertimbangkan overflow).
3
@ Shaz: Itu benar untuk skrip pengujian, tetapi bukan bagian dari pertanyaan yang diajukan.
21

Dalam penambahan posisi, pengurangan dan perkalian bilangan unsigned untuk menghasilkan hasil unsigned, digit masukan yang lebih signifikan tidak mempengaruhi digit yang kurang signifikan dari hasil. Ini berlaku untuk aritmatika biner seperti halnya pada aritmatika desimal. Ini juga berlaku untuk aritmatika bertanda "dua komplemen", tetapi tidak untuk aritmatika bertanda besar-tanda.

Namun kita harus berhati-hati saat mengambil aturan dari aritmatika biner dan menerapkannya ke C (saya percaya C ++ memiliki aturan yang sama dengan C pada hal ini tetapi saya tidak yakin 100%) karena aritmatika C memiliki beberapa aturan misterius yang dapat membuat kita tersandung naik. Aritmatika tak bertanda tangan di C mengikuti aturan pembungkus biner sederhana tetapi luapan aritmatika bertanda tangan adalah perilaku tak terdefinisi. Lebih buruk dalam beberapa keadaan C akan secara otomatis "mempromosikan" tipe unsigned ke (signed) int.

Perilaku tidak terdefinisi di C bisa sangat berbahaya. Kompilator yang bodoh (atau kompilator dengan tingkat pengoptimalan rendah) kemungkinan besar akan melakukan apa yang Anda harapkan berdasarkan pemahaman Anda tentang aritmatika biner, sementara kompilator pengoptimalan dapat merusak kode Anda dengan cara yang aneh.


Jadi kembali ke rumus dalam pertanyaan kesetaraan tergantung pada jenis operan.

Jika mereka adalah unsigned integers yang ukurannya lebih besar dari atau sama dengan ukuran int maka perilaku overflow dari operator penjumlahan didefinisikan dengan baik sebagai sampul biner sederhana. Apakah kita menutupi atau tidak 24 bit tinggi dari satu operan sebelum operasi penambahan tidak berdampak pada bit rendah hasil.

Jika mereka adalah unsigned integers yang ukurannya kurang dari intmaka mereka akan dipromosikan menjadi (ditandatangani) int. Limpahan bilangan bulat yang ditandatangani adalah perilaku yang tidak ditentukan tetapi setidaknya pada setiap platform saya telah menemukan perbedaan ukuran antara jenis bilangan bulat yang berbeda cukup besar sehingga satu penambahan dua nilai yang dipromosikan tidak akan menyebabkan luapan. Jadi sekali lagi kita dapat kembali ke argumen aritmatika biner sederhana untuk menganggap pernyataan tersebut setara.

Jika mereka adalah bilangan bulat bertanda yang ukurannya kurang dari int maka luapan lagi tidak dapat terjadi dan pada implementasi pelengkap dua kita dapat mengandalkan argumen aritmatika biner standar untuk mengatakan bahwa mereka ekuivalen. Pada besarnya tanda atau yang melengkapi implementasi mereka tidak akan sama.

OTOH jika adan bditandatangani bilangan bulat yang ukurannya lebih besar dari atau sama dengan ukuran int maka bahkan pada dua implementasi pelengkap ada kasus di mana satu pernyataan akan terdefinisi dengan baik sementara yang lain akan menjadi perilaku tidak terdefinisi.

plugwash
sumber
20

Lemma: a & 255 == a % 256untuk unsigned a.

Unsigned adapat ditulis kembali sebagai m * 0x100 + bbeberapa unsigned m, b, 0 <= b < 0xff, 0 <= m <= 0xffffff. Ini mengikuti dari kedua definisi itua & 255 == b == a % 256 .

Selain itu, kami membutuhkan:

  • properti distributif: (a + b) mod n = [(a mod n) + (b mod n)] mod n
  • definisi penjumlahan unsigned, secara matematis: (a + b) ==> (a + b) % (2 ^ 32)

Jadi:

(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

Jadi ya, memang benar. Untuk integer 32-bit unsigned.


Bagaimana dengan tipe integer lainnya?

  • Untuk 64-bit unsigned integer, semua hal di atas hanya berlaku juga, hanya mengganti 2^64untuk 2^32.
  • Untuk unsigned integer 8 dan 16-bit, penambahan melibatkan promosi ke int. Ini intpasti tidak akan meluap atau negatif dalam salah satu operasi ini, jadi semuanya tetap valid.
  • Untuk bilangan bulat bertanda , jika salah satu a+batau a+(b&255)meluap, ini adalah perilaku yang tidak ditentukan. Jadi kesetaraan tidak bisa berlaku - ada kasus di mana (a+b)&255perilaku tidak terdefinisi tetapi (a+(b&255))&255tidak.
Barry
sumber
17

Ya, (a + b) & 255baiklah.

Ingat penambahan di sekolah? Anda menambahkan angka digit demi digit, dan menambahkan nilai carry ke kolom digit berikutnya. Kolom digit selanjutnya (yang lebih signifikan) tidak dapat memengaruhi kolom yang sudah diproses. Karena itu, tidak ada bedanya jika Anda mengosongkan digit hanya di hasil, atau juga pertama dalam argumen.


Hal di atas tidak selalu benar, standar C ++ memungkinkan implementasi yang akan merusaknya.

Seperti Deathstation 9000 : - ) harus menggunakan 33-bit int, jika OP berarti unsigned short"32-bit unsigned integers". Jika unsigned intdimaksudkan, DS9K harus menggunakan 32-bit int, dan 32-bit unsigned intdengan bit padding. (Bilangan bulat unsigned harus memiliki ukuran yang sama dengan bilangan yang ditandatangani sesuai §3.9.1 / 3, dan bit padding diperbolehkan dalam §3.9.1 / 1.) Kombinasi lain dari ukuran dan bit padding juga akan berfungsi.

Sejauh yang saya tahu, inilah satu-satunya cara untuk memecahkannya, karena:

  • Representasi integer harus menggunakan skema encoding "murni biner" (§3.9.1 / 7 dan catatan kaki), semua bit kecuali bit padding dan bit tanda harus memberikan nilai 2 n
  • promosi int diperbolehkan hanya jika int dapat mewakili semua nilai dari tipe sumber (§4.5 / 1), jadi intharus memiliki setidaknya 32 bit yang berkontribusi pada nilai, ditambah bit tanda.
  • yang inttidak dapat memiliki nilai lebih bit (tidak termasuk bit tanda) dari 32, karena yang lain tambahan tidak bisa meluap.
alain
sumber
2
Ada banyak operasi lain selain penambahan di mana sampah di bit tinggi tidak mempengaruhi hasil di bit rendah yang Anda minati. Lihat Tanya Jawab tentang pelengkap 2 , yang menggunakan x86 asm sebagai kasus penggunaan, tetapi juga berlaku untuk bilangan bulat biner unsigned dalam situasi apa pun.
Peter Cordes
2
Meskipun tentu saja setiap orang berhak untuk tidak memberi suara secara anonim, saya selalu menghargai komentar sebagai kesempatan untuk belajar.
alain
2
Sejauh ini, ini adalah jawaban / argumen termudah untuk dipahami, IMO. Penambahan / pengurangan carry / pinjam menyebar hanya dari bit rendah ke bit tinggi (kanan ke kiri) dalam biner, sama seperti dalam desimal. IDK mengapa seseorang akan downvote ini.
Peter Cordes
1
@Bathsheba: CHAR_BIT tidak harus 8. Tetapi tipe unsigned di C dan C ++ diperlukan untuk berperilaku seperti bilangan bulat biner base2 normal dengan lebar beberapa bit. Saya pikir itu membutuhkan UINT_MAX itu 2^N-1. (N bahkan tidak diperlukan untuk menjadi kelipatan CHAR_BIT, saya lupa, tapi saya cukup yakin standar mengharuskan sampul terjadi modulo beberapa kekuatan 2.) Saya pikir satu-satunya cara Anda bisa mendapatkan keanehan adalah melalui promosi ke tipe bertanda tangan yang cukup lebar untuk menampung aatau btetapi tidak cukup lebar untuk dipegang a+bdalam semua kasus.
Peter Cordes
2
@Bathsheba: ya, untungnya bahasa C-as-portable-assembly benar-benar berfungsi untuk tipe unsigned. Bahkan implementasi C yang sengaja dimusuhi tidak dapat merusak ini. Itu hanya tipe bertanda di mana hal-hal mengerikan untuk bit-hack yang benar-benar portabel di C, dan Deathstation 9000 benar-benar dapat memecahkan kode Anda.
Peter Cordes
14

Anda sudah memiliki jawaban cerdas: aritmatika unsigned adalah aritmatika modulo dan oleh karena itu hasilnya akan bertahan, Anda dapat membuktikannya secara matematis ...


Namun, satu hal keren tentang komputer adalah komputer itu cepat. Memang, mereka sangat cepat sehingga menghitung semua kombinasi valid dari 32 bit dimungkinkan dalam jumlah waktu yang wajar (jangan coba dengan 64 bit).

Jadi, dalam kasus Anda, saya pribadi suka membuangnya ke komputer; saya membutuhkan lebih sedikit waktu untuk meyakinkan diri sendiri bahwa program itu benar daripada yang diperlukan untuk meyakinkan diri sendiri daripada bukti matematis yang benar dan bahwa saya tidak mengawasi detail dalam spesifikasi 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;
}

Ini menghitung melalui semua kemungkinan nilai adanb dalam ruang 32-bit dan memeriksa apakah kesetaraan berlaku, atau tidak. Jika tidak, ia mencetak kasus yang tidak berfungsi, yang dapat Anda gunakan sebagai pemeriksaan kewarasan.

Dan, menurut Clang : Kesetaraan berlaku .

Selanjutnya, mengingat bahwa aturan aritmatika adalah bit-width agnostic (di atas intbit-width), persamaan ini akan berlaku untuk semua tipe integer 32 bit atau lebih yang tidak bertanda tangan, termasuk 64 bit dan 128 bit.

Catatan: Bagaimana kompilator menyebutkan semua pola 64-bit dalam kerangka waktu yang wajar? Itu tidak bisa. Loop dioptimalkan. Kalau tidak, kita semua akan mati sebelum eksekusi dihentikan.


Saya awalnya hanya membuktikannya untuk 16-bit unsigned integers; sayangnya C ++ adalah bahasa yang tidak masuk akal di mana bilangan bulat kecil (bitwidth lebih kecil dari int) pertama kali dikonversi 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;
}

Dan sekali lagi, menurut Clang : Kesetaraan berlaku .

Nah, ini dia :)


1 Tentu saja, jika suatu program secara tidak sengaja memicu Perilaku Tidak Terdefinisi, itu tidak akan membuktikan banyak.

Matthieu M.
sumber
1
Anda mengatakan itu mudah dilakukan dengan nilai 32-bit tetapi sebenarnya menggunakan 16-bit ...: D
Willi Mentzel
1
@WilliMentzel: Itu pernyataan yang menarik. Awalnya saya ingin mengatakan bahwa jika berfungsi dengan 16 bit maka itu akan bekerja sama dengan 32 bit, 64 bit dan 128 bit karena Standar tidak memiliki perilaku khusus untuk lebar bit yang berbeda ... namun saya ingat bahwa sebenarnya demikian untuk bit-width lebih kecil dari int: bilangan bulat kecil pertama-tama diubah menjadi int(aturan aneh). Jadi saya sebenarnya harus melakukan demonstrasi dengan 32-bit (dan setelah itu meluas ke 64 bit, 128 bit, ...).
Matthieu M.
2
Karena Anda tidak dapat mengevaluasi semua (4294967296 - 1) * (4294967296 - 1) kemungkinan hasil, entah bagaimana Anda mengurangi? Menurut saya, MAX seharusnya (4294967296 - 1) jika Anda pergi ke sana tetapi tidak akan pernah selesai dalam masa hidup kita seperti yang Anda katakan ... jadi, bagaimanapun, kita tidak dapat menunjukkan kesetaraan dalam percobaan, setidaknya tidak dalam percobaan seperti Anda menggambarkan.
Willi Mentzel
1
Menguji ini pada implementasi pelengkap satu 2 tidak membuktikan bahwa itu portabel untuk ukuran atau pelengkap seseorang dengan lebar tipe Deathstation 9000. misalnya tipe unsigned yang sempit dapat dipromosikan menjadi 17-bit intyang dapat mewakili setiap kemungkinan uint16_t, tetapi di mana a+bdapat meluap. Itu hanya masalah untuk tipe unsigned yang lebih sempit dari int; C mensyaratkan bahwa unsignedtipe adalah bilangan bulat biner, sehingga terjadi sampul modulo dengan pangkat 2
Peter Cordes
1
Setuju tentang C yang terlalu portabel untuk kebaikannya sendiri. Akan sangat bagus jika mereka membuat standarisasi pada komplemen 2, pergeseran kanan aritmatika untuk bertanda tangan, dan cara untuk melakukan aritmatika bertanda tangan dengan membungkus semantik alih-alih semantik perilaku tidak terdefinisi, untuk kasus-kasus ketika Anda ingin membungkus. Kemudian C sekali lagi dapat berguna sebagai assembler portabel, bukan ladang ranjau berkat kompiler pengoptimalan modern yang membuatnya tidak aman untuk meninggalkan perilaku tidak terdefinisi (setidaknya untuk platform target Anda. Perilaku tidak terdefinisi hanya pada implementasi Deathstation 9000 tidak masalah, karena Anda menunjukkan).
Peter Cordes
4

Jawaban singkatnya adalah: kedua ekspresi itu setara

  • karena adan bmerupakan bilangan bulat unsigned 32-bit, hasilnya sama bahkan jika terjadi luapan. aritmatika unsigned menjamin ini: hasil yang tidak dapat diwakili oleh jenis bilangan bulat unsigned yang dihasilkan dikurangi modulo bilangan yang satu lebih besar dari nilai terbesar yang dapat diwakili oleh jenis yang dihasilkan.

Jawaban panjangnya adalah: tidak ada platform yang diketahui di mana ekspresi ini akan berbeda, tetapi Standar tidak menjaminnya, karena aturan promosi yang tidak terpisahkan.

  • Jika jenis adan b(unsigned 32 bit integers) memiliki peringkat lebih tinggi dari int, komputasi dilakukan sebagai unsigned, modulo 2 32 , dan menghasilkan hasil yang sama untuk kedua ekspresi untuk semua nilai adan b.

  • Sebaliknya, Jika tipe adan blebih kecil dari int, keduanya dipromosikan ke intdan penghitungan dilakukan menggunakan aritmatika bertanda, di mana luapan memunculkan perilaku yang tidak ditentukan.

    • Jika intmemiliki setidaknya 33 bit nilai, tidak satu pun dari ekspresi di atas dapat meluap, sehingga hasilnya ditentukan dengan sempurna dan memiliki nilai yang sama untuk kedua ekspresi.

    • Jika intmemiliki tepat 32 bit nilai, komputasi dapat meluap untuk kedua ekspresi, sebagai contoh nilai a=0xFFFFFFFFdan b=1akan menyebabkan luapan di kedua ekspresi. Untuk menghindari hal ini, Anda perlu menulis ((a & 255) + (b & 255)) & 255.

  • Kabar baiknya adalah tidak ada platform seperti itu 1 .


1 Lebih tepatnya, tidak ada platform nyata seperti itu, tetapi DS9K dapat dikonfigurasi untuk menunjukkan perilaku seperti itu dan masih sesuai dengan Standar C.

chqrlie.dll
sumber
3
Subbullet ke-2 Anda membutuhkan (1) alebih kecil dari int(2) intmemiliki 32 bit nilai (3) a=0xFFFFFFFF. Itu tidak mungkin semua benar.
Barry
1
@ Barry: Satu kasus yang tampaknya memenuhi persyaratan adalah 33-bit int, di mana terdapat 32 bit nilai dan bit satu tanda.
Ben Voigt
2

Identik dengan asumsi tidak melimpah . Tidak ada versi yang benar-benar kebal terhadap luapan tetapi versi ganda dan versi lebih tahan terhadapnya. Saya tidak mengetahui sistem di mana luapan dalam kasus ini merupakan masalah, tetapi saya dapat melihat penulis melakukan ini jika ada.

Loren Pechtel
sumber
1
OP ditentukan: (a dan b adalah 32-bit unsigned integers) . Kecuali jika intlebarnya 33 bit, hasilnya sama bahkan jika terjadi overflow. aritmatika unsigned menjamin ini: hasil yang tidak dapat diwakili oleh jenis bilangan bulat unsigned yang dihasilkan dikurangi modulo bilangan yang satu lebih besar dari nilai terbesar yang dapat diwakili oleh jenis yang dihasilkan.
chqrlie
2

Ya, Anda dapat membuktikannya dengan aritmatika, tetapi ada jawaban yang lebih intuitif.

Saat menambahkan, setiap bit hanya memengaruhi yang lebih signifikan daripada dirinya sendiri; tidak pernah kurang signifikan.

Oleh karena itu, apa pun yang Anda lakukan pada bit yang lebih tinggi sebelum penambahan tidak akan mengubah hasilnya, selama Anda hanya menyimpan bit yang kurang signifikan daripada bit terendah yang dimodifikasi.

Francesco Dondi
sumber
0

Buktinya sepele dan dibiarkan sebagai latihan bagi pembaca

Tetapi untuk benar-benar melegitimasi ini sebagai jawaban, baris kode pertama Anda mengatakan ambil 8 bit terakhir dari b** (semua bit lebih tinggi dari bset ke nol) dan tambahkan ini adan kemudian ambil hanya 8 bit terakhir dari pengaturan hasil semua lebih tinggi bit ke nol.

Baris kedua mengatakan tambahkan adan bdan ambil 8 bit terakhir dengan semua bit yang lebih tinggi nol.

Hanya 8 bit terakhir yang signifikan dalam hasil. Oleh karena itu hanya 8 bit terakhir yang signifikan pada masukan.

** 8 bit terakhir = 8 LSB

Juga menarik untuk dicatat bahwa outputnya akan sama dengan

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

Seperti di atas, hanya 8 LSB yang signifikan, tetapi hasilnya adalah an unsigned intdengan semua bit lainnya nol. The a + bakan meluap, menghasilkan hasil yang diharapkan.

pengguna3728501
sumber
Tidak, tidak akan. Matematika karakter terjadi karena int dan char dapat ditandatangani.
Antti Haapala