((A + (b & 255)) & 255), ((a + b) & 255) ile aynı mı?


92

Bazı C ++ kodlarına göz atıyordum ve şuna benzer bir şey buldum:

(a + (b & 255)) & 255

Çifte VE beni kızdırdı, ben de düşündüm:

(a + b) & 255

( ave b32 bitlik işaretsiz tam sayılardır)

Teorimi doğrulamak için hızlıca bir test komut dosyası (JS) yazdım:

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

Senaryo hipotezimi doğrulasa da (her iki işlem de eşittir), buna hala güvenmiyorum, çünkü 1) rastgele ve 2) Ben matematikçi değilim, ne yaptığım hakkında hiçbir fikrim yok .

Ayrıca Lisp-y başlığı için özür dilerim. Düzenlemekten çekinmeyin.


4
Bu senaryo hangi dil? Math.random()[0,1) üzerinde bir tamsayı mı yoksa bir çift mi döndürür? Senaryonuzun (söyleyebileceğim en iyisi) oluşturduğunuz sorunu yansıttığını sanmıyorum.
Brick

7
C / c ++ kodu nedir? Farklı diller.
Weather Vane

14
JS'de test etmeye çalıştığınız davranışı yeniden oluşturamazsınız. Bu yüzden dil seçimi konusunda herkes sadece sizsiniz. JS güçlü bir şekilde yazılmamıştır ve cevap kritik olarak C / C ++ 'daki değişkenlerin türüne bağlıdır. JS, sorduğunuz soru göz önüne alındığında tamamen saçmadır.
Brick

4
@WeatherVane Javascript işlev adlarını kullanan sözde kod önemlidir. Sorusu, C ve C ++ 'da işaretsiz tamsayıların davranışı &ve +onlarla ilgili.
Barmar

11
"Bir test programı yazdım ve olası tüm girdiler için beklediğim yanıtı aldım" ifadesinin aslında bir şeyin beklediğiniz gibi davrandığının garantisi olmadığını unutmayın. Tanımlanmamış davranışlar bu kadar kötü olabilir; yalnızca kodunuzun doğru olduğuna kendinizi ikna ettikten sonra beklenmedik sonuçlar verir.

Yanıtlar:


78

Onlar aynı. İşte bir kanıt:

İlk önce kimliği not edin (A + B) mod C = (A mod C + B mod C) mod C

Sorunu a & 255, ayakta durmak olarak ele alalım a % 256. aİmzasız olduğu için bu doğrudur .

Yani (a + (b & 255)) & 255bir(a + (b % 256)) % 256

Bu, (a % 256 + b % 256 % 256) % 256(Yukarıda belirtilen kimliği uyguladım: not edin modve %imzasız türler için eşdeğerdir.)

Bu, (a % 256 + b % 256) % 256hangisinin (a + b) % 256(kimliğin yeniden uygulanması) olduğunu basitleştirir . Daha sonra bitsel operatörü geri koyabilirsiniz.

(a + b) & 255

kanıtı tamamlamak.


81
Taşma olasılığını göz ardı eden matematiksel bir kanıttır. Düşünün A=0xFFFFFFFF, B=1, C=3. İlk kimlik geçerli değil. (Taşma işaretsiz aritmetik için bir sorun olmayacak, ancak biraz farklı bir şey.)
AlexD

4
Aslında, (a + (b & 255)) & 255aynı (a + (b % 256)) % N % 256yerde, Nmaksimum işaretsiz değer birden büyüktür. (son formülün matematiksel tam sayıların aritmetiği olarak yorumlanması amaçlanmıştır)

17
Bunun gibi matematiksel ispatlar bilgisayar mimarilerinde tamsayıların davranışını ispatlamak için uygun değildir.
Jack Aidley

25
@JackAidley: Doğru yapıldığında uygundurlar (ki bu, taşmayı dikkate almayı ihmal etmekten dolayı değildir).

3
@Shaz: Bu test senaryosu için doğru, ancak sorulan sorunun bir parçası değil.

21

İşaretsiz sonuçlar elde etmek için işaretsiz sayıların konumsal toplama, çıkarma ve çarpma işlemlerinde, girişin daha anlamlı basamakları sonucun daha az önemli basamaklarını etkilemez. Bu, ikili aritmetik için olduğu kadar ondalık aritmetik için de geçerlidir. Aynı zamanda "iki tamamlayıcı" işaretli aritmetik için de geçerlidir, ancak işaret büyüklüğü işaretli aritmetik için geçerli değildir.

Bununla birlikte, ikili aritmetikten kuralları alırken ve bunları C'ye uygularken dikkatli olmalıyız (C ++ bu şeyde C ile aynı kurallara sahiptir, ancak% 100 emin değilim) çünkü C aritmetiğinin bizi tökezletebilecek bazı gizli kuralları vardır. yukarı. C'deki işaretsiz aritmetik basit ikili sarmalama kurallarını izler, ancak işaretli aritmetik taşma tanımsız bir davranıştır. Bazı durumlarda daha kötüsü C, işaretsiz bir türü otomatik olarak (imzalı) int olarak "yükseltir".

C'de tanımlanmamış davranış özellikle önemsiz olabilir. Aptal bir derleyici (veya düşük optimizasyon düzeyine sahip bir derleyici) muhtemelen ikili aritmetik anlayışınıza dayanarak beklediğiniz şeyi yapacaktır, ancak optimize eden bir derleyici kodunuzu garip şekillerde kırabilir.


Dolayısıyla, sorudaki formüle geri dönersek, eşdeğerlik işlenen türlerine bağlıdır.

Boyutları boyutundan büyük veya boyutuna eşit olan işaretsiz tamsayılarsa int, toplama işlecinin taşma davranışı basit ikili sarmalama olarak iyi tanımlanır. Toplama işleminden önce bir işlenenin yüksek 24 bitini maskeleyip gizlemememizin sonucun düşük bitleri üzerinde hiçbir etkisi yoktur.

Boyutu daha küçük olan işaretsiz tamsayılarsa, int(imzalı) olarak yükseltileceklerdir int. İşaretli tamsayıların taşması tanımlanmamış bir davranıştır, ancak en azından karşılaştığım her platformda farklı tam sayı türleri arasındaki boyut farkı, iki yükseltilmiş değerin tek bir eklenmesi taşmaya neden olmayacak kadar büyük. Dolayısıyla, ifadeleri eşdeğer olarak kabul etmek için basit ikili aritmetik argümana geri dönebiliriz.

Boyutları int'den daha küçük olan işaretli tamsayılarsa, tekrar taşma gerçekleşemez ve iki tamamlayıcı uygulamalara eşit olduklarını söylemek için standart ikili aritmetik argümanına güvenebiliriz. İşaret büyüklüğü veya tamamlayıcı uygulamalar konusunda, bunlar eşit olmaz.

OTOH eğer ave bkimin büyüklüğündeydi daha büyük ve hatta ikiler tamamlayıcı uygulamaları daha sonra int büyüklüğüne eşit diğer tanımlanmamış bir davranış olur iken bir ifade iyi tanımlanmış olacağını durumlar vardır tamsayılar imzalandı.


20

Lemma: a & 255 == a % 256işaretsiz için a.

İmzasız aolarak yeniden olabilir m * 0x100 + bbazı imzasız m, b, 0 <= b < 0xff, 0 <= m <= 0xffffff. Her iki tanımdan da bunu izler a & 255 == b == a % 256.

Ek olarak şunlara ihtiyacımız var:

  • dağıtım özelliği: (a + b) mod n = [(a mod n) + (b mod n)] mod n
  • matematiksel olarak işaretsiz toplamanın tanımı: (a + b) ==> (a + b) % (2 ^ 32)

Böylece:

(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

Yani evet, doğru. 32 bitlik işaretsiz tamsayılar için.


Peki ya diğer tam sayı türleri?

  • 64 bit işaretsiz tamsayıları için, her şeyden sadece ikame ki öyle de geçerlidir 2^64için 2^32.
  • 8 ve 16 bitlik işaretsiz tamsayılar için ekleme int,. Bu int, bu işlemlerin hiçbirinde kesinlikle ne aşmayacak ne de olumsuz olacaktır, dolayısıyla hepsi geçerliliğini korur.
  • İçin imzalanmış ise ya tamsayılar, a+bya a+(b&255)taşma, tanımsız bir davranış. Dolayısıyla eşitlik geçerli olamaz - (a+b)&255tanımsız davranışın olduğu ancak (a+(b&255))&255olmadığı durumlar vardır .

17

Evet, (a + b) & 255güzel.

Okuldaki eklemeyi hatırlıyor musun? Sayıları basamak basamak eklersiniz ve sonraki basamak sütununa bir taşıma değeri eklersiniz. Daha sonraki (daha önemli) bir basamak sütununun önceden işlenmiş bir sütunu etkilemesinin bir yolu yoktur. Bu nedenle, rakamları yalnızca sonuçta veya bir argümanda ilk önce sıfırlarsanız bir fark yaratmaz.


Yukarıdakiler her zaman doğru değildir, C ++ standardı bunu bozacak bir uygulamaya izin verir.

Böyle bir Deathstation 9000 : - )int , eğer OP unsigned short"32-bit işaretsiz tamsayılar" ile kastediliyorsa, 33-bit kullanmalıdır . Eğer unsigned intkastedilmiş olsaydı, DS9K intbir 32-bit unsigned intve bir dolgu biti ile bir 32-bit kullanmak zorunda kalacaktı . (İşaretsiz tamsayıların, §3.9.1 / 3'e göre imzalı emsalleriyle aynı boyuta sahip olması gerekir ve §3.9.1 / 1'de doldurma bitlerine izin verilir.) Diğer boyut ve dolgu bitleri kombinasyonları da işe yarayacaktır.

Anladığım kadarıyla, onu kırmanın tek yolu bu, çünkü:

  • Tamsayı gösterimi "tamamen ikili" bir kodlama şeması (§3.9.1 / 7 ve dipnot) kullanmalıdır, dolgu bitleri hariç tüm bitler ve işaret biti 2 n değerine katkıda bulunmalıdır
  • int promosyonuna yalnızca intkaynak türünün (§4.5 / 1) tüm değerlerini temsil edebiliyorsa izin verilir , bu nedenle intdeğere katkıda bulunan en az 32 bit artı bir işaret biti içermelidir.
  • intçünkü başka bir ekleme taşma olamaz, 32'den (işaret bitini hariç) daha fazla değer bit olamaz.

2
Birçok diğer işlemler vardır yüksek bit çöp sen ilgilenen yılında. Bkz düşük bit sonucu etkilemez ek yanında 2'nin tamamlayıcı ilgili bu Q & A kullanım örneği olarak x86 asm kullanır, fakat aynı zamanda uygulanır herhangi bir durumda işaretsiz ikili tamsayılar.
Peter Cordes

2
Elbette anonim olarak olumsuz oy kullanmak herkesin hakkı olsa da, bir yorumu öğrenme fırsatı olarak her zaman takdir ediyorum.
alain

2
Bu anlaşılması en kolay cevap / argümandır, IMO. Toplama / çıkarmada taşıma / ödünç alma, ondalıkta olduğu gibi, ikili olarak yalnızca düşük bitlerden yüksek bitlere (sağdan sola) yayılır. IDK neden birisi buna olumsuz oy versin.
Peter Cordes

1
@Bathsheba: CHAR_BIT'in 8 olması gerekmez. Ancak C ve C ++ 'daki işaretsiz türlerin, bazı bit genişliğindeki normal baz2 ikili tamsayılar gibi davranması gerekir. Bence bu UINT_MAX gerektiriyor 2^N-1. (N'nin CHAR_BIT'in bir katı olması bile gerekmeyebilir, unutuyorum, ama standardın 2'lik bir güç modulo sarma olmasını gerektirdiğinden eminim.) Bence tuhaflık elde etmenin tek yolu Her durumda tutacak kadar geniş aveya tutacak kadar geniş olmayan imzalı tip . ba+b
Peter Cordes

2
@Bathsheba: evet, neyse ki C-as-portable-assembly-dili gerçekten çoğunlukla imzasız tipler için çalışıyor. Kasten düşmanca bir C uygulaması bile bunu kıramaz. Yalnızca C'deki gerçekten taşınabilir bit hackler için işlerin korkunç olduğu imzalı türlerdir ve bir Deathstation 9000, kodunuzu gerçekten kırabilir.
Peter Cordes

14

Zaten akıllı cevaba sahipsiniz: işaretsiz aritmetik, modülo aritmetiktir ve bu nedenle sonuçlar geçerli olacaktır, bunu matematiksel olarak kanıtlayabilirsiniz ...


Bilgisayarlarla ilgili harika bir şey, bilgisayarların hızlı olmasıdır. Aslında, o kadar hızlıdırlar ki, 32 bitlik tüm geçerli kombinasyonları makul bir süre içinde saymak mümkündür (64 bit ile denemeyin).

Bu yüzden, sizin durumunuzda, ben şahsen onu bir bilgisayara atmayı seviyorum; o program matematiksel kanıtı doğru olandan kendimi ikna için gereken daha doğru olduğunu kendimi ikna etmeye bana daha az zaman alır ve ben şartnamede bir ayrıntıyı nezaret etmediğini 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;
}

Bu, tüm olası değerleri ile numaralandırır aveb eşitlik tutan veya değil 32-bit uzayda ve çekler olsun. Çalışmazsa, mantık kontrolü olarak kullanabileceğiniz, çalışmayan vakayı yazdırır.

Ve Clang'a göre : Eşitlik geçerlidir .

Ayrıca, aritmetik kuralların bit genişliği agnostik olduğu göz önüne alındığında (yukarıda int bit genişliğinin ) , bu eşitlik, 64 bit ve 128 bit dahil olmak üzere 32 bit veya daha fazla işaretsiz tamsayı türü için geçerli olacaktır.

Not: Bir derleyici, tüm 64 bitlik kalıpları makul bir zaman diliminde nasıl sıralayabilir? Olamaz. Döngüler optimize edildi. Aksi takdirde idam sona ermeden hepimiz ölmüş olurduk.


Başlangıçta bunu yalnızca 16 bitlik işaretsiz tamsayılar için kanıtladım; ne yazık ki C ++, küçük tam sayıların (bundan daha küçük bit genişlikleri int) ilk önce dönüştürüldüğü çılgın bir dildir 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;
}

Ve bir kez daha, Clang'a göre : Eşitlik geçerlidir .

İyi gidiyorsun :)


1 Elbette, bir program yanlışlıkla Tanımlanmamış Davranışı tetiklerse, pek bir şey kanıtlamaz.


1
32 bit değerlerle yapmanın kolay olduğunu söylüyorsunuz ama aslında 16 bit kullanıyorsunuz ...: D
Willi Mentzel

1
@WilliMentzel: Bu ilginç bir açıklama. Başlangıçta 16 bit ile çalışıyorsa 32 bit, 64 bit ve 128 bit ile aynı şekilde çalışacağını söylemek istedim çünkü Standardın farklı bit genişlikleri için belirli bir davranışı yoktur ... ancak aslında olduğunu hatırladım şundan daha küçük bit genişlikleri için int: küçük tamsayılar önce dönüştürülür int(garip bir kural). Yani aslında gösterimi 32 bit ile yapmam gerekiyor (ve daha sonra 64 bit, 128 bit, ...).
Matthieu M.

2
Tüm (4294967296 - 1) * (4294967296 - 1) olası sonuçları değerlendiremediğiniz için, bir şekilde azaltıyorsunuz? Benim düşünceme göre MAX (4294967296 - 1) olmalı, eğer böyle gidersen ama dediğin gibi hayatımız boyunca asla bitmeyecek ... yani sonuçta eşitliği bir deneyde gösteremeyiz, en azından senin gibi birinde tanımlamak.
Willi Mentzel

1
Bunu, bir 2'nin tamamlayıcı uygulaması üzerinde test etmek, işaret büyüklüğü için taşınabilir olduğunu veya Deathstation 9000 tipi genişliklerle tamamlayıcı olduğunu kanıtlamaz. örneğin dar bir işaretsiz tür, intmümkün olan her şeyi temsil edebilen uint16_t, ancak nerede a+btaşabilir ki 17-bit'e yükselebilir. Bu sadece işaretsiz tipler için daha dar olan bir sorundur int; C, unsignedtürlerin ikili tamsayılar olmasını gerektirir , bu nedenle, 2'nin kuvveti modulo olur
Peter Cordes

1
C'nin kendi iyiliği için fazla taşınabilir olduğu konusunda hemfikir. Bu olurdu gerçekten ne zaman onlar bu durumlar için, sarma semantik yerine tanımsız-davranış semantik ile aritmetik imzalı edersiniz 2'nin tamamlayıcı, imzalı için aritmetik sağ vardiya ve yolda standardize olsaydınız güzel istiyorum sarma. O zaman C, herhangi bir tanımsız davranışı (en azından hedef platformunuz için) bırakmayı güvensiz hale getiren modern optimizasyon derleyicileri sayesinde bir mayın tarlası yerine taşınabilir bir montajcı olarak bir kez daha yararlı olabilir. işaret etmek).
Peter Cordes

4

Hızlı cevap: her iki ifade de eşdeğerdir

  • beri ave b32 bit işaretsiz tamsayı vardır, sonuç bile taşma durumunda aynıdır. işaretsiz aritmetik bunu garanti eder: Ortaya çıkan işaretsiz tamsayı türü ile temsil edilemeyen bir sonuç, sonuçta ortaya çıkan tür tarafından temsil edilebilecek en büyük değerden bir büyük olan sayı modulo azaltılır.

Uzun cevap şudur: Bu ifadelerin farklı olacağı bilinen bir platform yoktur, ancak Standart, bütünsel promosyon kuralları nedeniyle bunu garanti etmez.

  • aVe b(işaretsiz 32 bitlik tamsayılar) türü daha yüksek bir sıraya sahipse int, hesaplama işaretsiz olarak gerçekleştirilir, modulo 2 32 ve tüm ave değerleri için her iki ifade için de aynı tanımlanmış sonucu verir b.

  • Tersine, türü ave bdaha küçükse int, her ikisi de yükseltilir intve hesaplama işaretli aritmetik kullanılarak gerçekleştirilir, burada taşma tanımlanmamış davranışı çağırır.

    • Eğer intsonuç mükemmel tanımlandığı böylece taşabilir, ne Yukarıdaki ifadelerde en az 33 değeri bit olan ve her iki ifadeler için aynı değere sahiptir.

    • Eğer inttam 32 değeri bit olan, hesaplama olabilir için taşma iki örnek değerleri için, ifadeler a=0xFFFFFFFFve b=1iki salgılama bir taşmasına neden olur. Bundan kaçınmak için yazmanız gerekir ((a & 255) + (b & 255)) & 255.

  • İyi haber şu ki böyle platformlar yok 1 .


1 Daha doğrusu, böyle bir gerçek platform yoktur, ancak bir DS9K bu tür davranışları sergileyecek ve yine de C Standardına uyacak şekilde yapılandırılabilir .


3
2. alt madde işaretiniz (1) ' ain int(2) intdeğerinden küçük olmasını gerektirir (3) a=0xFFFFFFFF. Bunların hepsi doğru olamaz.
Barry

1
@Barry: Gereksinimleri karşılayan tek durum 33 bittir int, burada 32 değer biti ve bir işaret biti vardır.
Ben Voigt

2

Taşma olmadığı varsayılarak aynı . Her iki sürüm de taşmaya karşı gerçek anlamda bağışık değildir, ancak çift ve sürüm buna karşı daha dirençlidir. Bu durumda bir taşmanın sorun olduğu bir sistemin farkında değilim, ancak yazarın bunu yaptığını görebiliyorum.


1
Belirtilen OP: (a ve b, 32 bitlik işaretsiz tam sayılardır) . int33 bit genişliğinde olmadığı sürece , taşma durumunda bile sonuç aynıdır . işaretsiz aritmetik bunu garanti eder: Ortaya çıkan işaretsiz tamsayı türü ile temsil edilemeyen bir sonuç, sonuçta ortaya çıkan tür tarafından temsil edilebilecek en büyük değerden bir büyük olan sayı modulo azaltılır.
chqrlie

2

Evet, bunu aritmetik ile kanıtlayabilirsiniz, ancak daha sezgisel bir cevap var.

Eklerken, her bit yalnızca kendisinden daha önemli olanları etkiler; asla daha az önemli olanlar.

Bu nedenle, yalnızca değiştirilen en düşük bitten daha az önemli olan bitleri tuttuğunuz sürece, eklemeden önce daha yüksek bitlere ne yaparsanız yapın sonucu değiştirmez.


0

Kanıt önemsizdir ve okuyucu için bir egzersiz olarak bırakılmıştır.

Ancak bunu bir cevap olarak gerçekten meşrulaştırmak için, ilk kod satırınız son 8 biti almanızı b( bsetin tüm yüksek bitlerini sıfıra) ve buna eklemenizi ave ardından sonuç ayarının yalnızca son 8 bitini almanızı söyler. bitleri sıfıra.

İkinci satır eklemek diyor ave btüm yüksek bit sıfır ile son 8 bit ve alır.

Sonuçta sadece son 8 bit önemlidir. Bu nedenle, giriş (ler) de sadece son 8 bit önemlidir.

** son 8 bit = 8 LSB

Ayrıca çıktının eşdeğer olacağına dikkat etmek ilginçtir.

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

Yukarıdaki gibi, yalnızca 8 LSB anlamlıdır, ancak sonuç unsigned intdiğer tüm bitlerle sıfırdır. a + bBeklenen sonuç üreten, taşar.


Hayır olmaz. Char math, int ve char imzalanabildiği için gerçekleşir.
Antti Haapala
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.