(((a +(b&255))&255)与((a + b)&255)是否相同?


92

我在浏览一些C ++代码,发现了以下内容:

(a + (b & 255)) & 255

双重AND惹恼了我,所以我想到了:

(a + b) & 255

ab是32位无符号整数)

我迅速编写了一个测试脚本(JS)来确认我的理论:

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

尽管脚本证实了我的假设(两个操作都相等),但我仍然不相信它,因为1)随机并且2)我不是数学家,我也不知道我在做什么

另外,对Lisp-y头衔感到抱歉。随时进行编辑。


4
该脚本是什么语言?是否Math.random()在[0,1)上返回整数或双精度数?我认为您的脚本(我能说的最好)根本无法反映您提出的问题。

7
什么是C / C ++代码?它们是不同的语言。
天气叶片

14
您无法重现要在JS中测试的行为。这就是每个人在语言选择上只有您一个的原因。JS不是强类型的,答案在很大程度上取决于C / C ++中变量的类型。鉴于您所提出的问题,JS完全是胡说八道。

4
@WeatherVane这是使用Javascript函数名称的基本伪代码。他的问题是有关的行为&,并+在C和C ++的无符号整数。
Barmar

11
请记住,“我编写了一个测试程序并获得了所有可能输入的期望答案”实际上并不能保证某些行为会达到您的期望。未定义的行为可能会令人讨厌。在说服自己代码正确之后,才给出意外的结果。

Answers:


78

他们是一样的。这是一个证明:

首先注意身份 (A + B) mod C = (A mod C + B mod C) mod C

让我们通过考虑a & 255代替来重述该问题a % 256。这是正确的,因为a是未签名的。

所以,(a + (b & 255)) & 255(a + (b % 256)) % 256

这与(a % 256 + b % 256 % 256) % 256(我已经应用了上面提到的身份:请注意,mod并且%对于无符号类型等效)。

这简化了(a % 256 + b % 256) % 256成为哪个对象(a + b) % 256(重新应用身份)。然后,您可以将按位运算符放回给

(a + b) & 255

完成证明。


81
这是数学证明,忽略了溢出的可能性。考虑一下A=0xFFFFFFFF, B=1, C=3。第一个身份不成立。(对于无符号算术来说,溢出不会成为问题,但这是有点不同的东西。)
AlexD

4
实际上,(a + (b & 255)) & 255与相同(a + (b % 256)) % N % 256,其中N比最大无符号值大一个。(后一个公式应被解释为数学整数的算术)

17
诸如此类的数学证明不适用于证明整数在计算机体系结构上的行为。
杰克·艾德利

25
@JackAidley:正确完成后它们是适当的(由于忽略了溢出,所以不正确)。

3
@Shaz:测试脚本是正确的,但不是所问问题的一部分。

21

在无符号数的位置加,减和乘以产生无符号结果中,输入的更高有效数字不会影响结果的低有效数字。这适用于二进制算术,也适用于十进制算术。它也适用于“二进制补码”符号算术,但不适用于符号幅度符号算术。

但是,在从二进制算术中获取规则并将其应用于C时,我们必须要小心(我相信C ++在这方面具有与C相同的规则,但我不是100%确信),因为C算术有一些奥术规则可以使我们绊倒向上。C语言中的无符号算术遵循简单的二进制环绕规则,但是有符号算术溢出是未定义的行为。在某些情况下,更糟糕的是,C会自动将无符号类型“提升”为(有符号)int。

C语言中未定义的行为可能尤其隐蔽。基于对二进制算术的理解,愚蠢的编译器(或优化级别较低的编译器)可能会完成您期望的工作,而优化的编译器可能会以奇怪的方式破坏代码。


因此,回到问题中的公式,等效性取决于操作数类型。

如果它们是无符号整数,且其大小大于或等于 int则加法运算符的溢出行为被定义为简单的二进制环绕。在加法运算之前,是否屏蔽一个操作数的高24位不会影响结果的低位。

如果它们是大小小于的无符号整数,int则它们将被提升为(signed)int。有符号整数的溢出是未定义的行为,但是至少在我遇到的每个平台上,不同整数类型之间的大小差异都足够大,以至于两个提升值的单个加法运算不会引起溢出。因此,我们可以再次回到简单的二进制算术参数来认为语句等效。

如果它们是大小小于int的有符号整数,则不会再次发生溢出,并且在二进制补码实现中,我们可以依靠标准二进制算术参数来表示它们是等效的。在符号幅度或补码实现时,它们将不是等效的。

OTOH如果ab是带符号的整数,其大小大于或等于int的大小,那么即使在二进制补码实现中,也有一种情况是其中一个语句定义良好,而另一个则未定义。


20

引理:a & 255 == a % 256用于未签名a

未签名a可以作为被改写m * 0x100 + b一些未签名的mb0 <= b < 0xff0 <= m <= 0xffffff。从这两个定义可以得出:a & 255 == b == a % 256

此外,我们需要:

  • 分配属性: (a + b) mod n = [(a mod n) + (b mod n)] mod n
  • 数学上无符号加法的定义: (a + b) ==> (a + b) % (2 ^ 32)

从而:

(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

是的,这是真的。对于32位无符号整数。


那其他整数类型呢?

  • 对于64位无符号整数,上述所有内容同样适用,只是替换2^642^32
  • 对于8位和16位无符号整数,加法涉及提升为intint在任何这些操作中,这绝对不会溢出或为负,因此它们都保持有效。
  • 对于带符号整数,如果是a+ba+(b&255)溢出,则是未定义的行为。因此,平等无法成立,在某些情况下(a+b)&255行为是未定义的,但(a+(b&255))&255不是。

17

是的,(a + b) & 255很好。

还记得学校里的加法吗?您逐位添加数字,并将进位值添加到下一列数字。后面的(更重要的)数字列无法影响已经处理的列。因此,如果仅在结果中或在参数中首先将数字清零,则不会有任何区别。


上面的情况并不总是正确的,C ++标准允许实现打破这一点的实现。

这样的Deathstation 9000 - 必须使用一个33位的int,如果OP的意思unsigned short与“32位无符号整数”。如果unsigned int要这样做,DS9K必须使用32位int,以及32位unsigned int带有填充位。(按照§3.9.1/ 3,无符号整数必须与其有符号整数相同,并且在§3.9.1/ 1中允许填充位。)大小和填充位的其他组合也可以使用。

据我所知,这是打破它的唯一方法,因为:

  • 整数表示形式必须使用“纯二进制”编码方案(第3.9.1 / 7节和脚注),除填充位和符号位以外的所有位的值必须为2 n
  • 仅当int提升int可以表示源类型的所有值(第4.5 / 1节)时才被允许,因此int必须至少有32位对该值起作用,再加上一个符号位。
  • int不能有更多的值的比特(不包括符号位)超过32,因为其他的加成不能溢出。

2
除加法外,还有许多其他操作,其中高位垃圾不会影响您感兴趣的低位结果。有关2的补码,请参见此问答,它使用x86 asm作为用例,但也适用于在任何情况下都是无符号的二进制整数。
彼得·科德斯

2
当然,每个人都有匿名匿名投票的权利,但我总是很高兴将评论作为学习的机会。
alain

2
到目前为止,这是IMO最容易理解的答案/论点。加/减的进位/借位仅以二进制的形式从低位传播到高位(从右到左),与十进制相同。IDK为什么有人会对此表示反对。
彼得·科德斯

1
@Bathsheba:不需要将CHAR_BIT设置为8。但是,在C和C ++中,无符号类型必须充当某些位宽的普通base2二进制整数。我认为这要求UINT_MAX为2^N-1。(我什至忘记了,甚至可能甚至不需要N是CHAR_BIT的倍数,但我很确定标准要求以2的幂为模进行环绕。)我认为,获得怪异的唯一方法是通过提升为签署类型的宽到足以容纳ab但不宽,足以容纳a+b于所有情况。
彼得·科德斯

2
@Bathsheba:是的,幸运的是C-as-portable-assembly-language实际上确实适用于无符号类型。即使是故意敌对的C实现也不能打破这一点。这是只有签署类型的情况,这些情况对于使用C进行真正可移植的位黑客而言是可怕的,并且Deathstation 9000确实可以破坏您的代码。
彼得·科德斯

14

您已经有了一个聪明的答案:无符号算术是模算术,因此结果将保持不变,您可以用数学方式证明它……


但是,关于计算机的一件很酷的事情是计算机速度很快。确实,它们是如此之快,以至于在合理的时间内可以枚举32位的所有有效组合(请勿尝试使用64位)。

因此,就您而言,我个人喜欢将其扔到计算机上。与说服自己而不是数学证明是正确的让我说服自己的时间相比,让我花费更少的时间,并且我没有监督规范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;
}

此枚举通过的所有可能值a,并b在32位空间并检查是否等号成立,或没有。如果没有,它将打印出无效的案件,您可以将其用作健全性检查。

而且,根据Clang所说平等成立

此外,假定算术规则是位宽度不可知的(int位宽度以上),则此等价关系适用于32位或更多(包括64位和128位)的任何无符号整数类型。

注意:编译器如何在合理的时间范围内枚举所有64位模式?这不可以。循环进行了优化。否则,我们将在执行终止之前全部死亡。


最初,我仅针对16位无符号整数证明了这一点。不幸的是,C ++是一种疯狂的语言,其中小整数(位宽小于int)首先被转换为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;
}

再次,根据锵等式成立

好吧,你去了:)


1 当然,如果程序无意中触发了“未定义行为”,那就证明不了什么。


1
您说使用32位值很容易,但实际上使用16位...:D
Willi Mentzel

1
@WilliMentzel:有趣的一句话。我最初想说的是,如果它可以使用16位,那么它将适用于32位,64位和128位,因为标准对于不同的位宽没有特定的行为……但是我记得实际上它确实可以对于小于以下内容的位宽int:首先将小整数转换为int(奇怪的规则)。因此,我实际上必须使用32位进行演示(然后将其扩展到64位,128位等)。
Matthieu M.

2
由于您无法评估所有(4294967296-1)*(4294967296-1)可能的结果,您会以某种方式减少费用吗?我认为如果采用这种方式,MAX的值应为(4294967296-1),但它永远不会像您所说的那样在我们的一生中完成...因此,毕竟我们无法在实验中证明平等,至少在像您这样的人中无法证明描述。
威利·曼策尔

1
在2的补码实现上进行测试并不能证明它具有Deathstation 9000类型宽度的符号幅度或补码的可移植性。例如,狭窄的无符号类型可以提升为17位int,可以表示所有可能的值uint16_t,但是a+b可以溢出。对于比窄的无符号类型,这只是一个问题intC要求unsigned类型是二进制整数,因此以2的幂为模进行环绕操作
Peter Cordes

1
同意C出于自身利益而过于可移植。这将是真的很好,如果他们会在2的补码,算术右移的签署,以及标准化的方式做包装的语义,而不是未定义行为语义符号算术,对于那些情况下,当你换行。然后C可以再次用作便携式汇编程序,而不是雷区,这要归功于现代优化的编译器,这些编译器使保留任何未定义的行为(至少对于您的目标平台而言是不安全的)是不安全的。只有在Deathstation 9000实现上,未定义的行为才可以,因为您指出)。
彼得·科德斯

4

快速的答案是:两个表达式都是等效的

  • 由于ab是32位无符号整数,因此即使发生溢出,结果也是相同的。无符号算术保证了这一点:不能用结果无符号整数类型表示的结果的模数要比可以用结果类型表示的最大值大1的模数减少。

长的答案是:没有已知的平台,这些表达会有所不同,但是由于整体提升的规则,该标准不能保证它。

  • 如果类型ab(无符号的32个整数)具有比较高的等级int,计算被视为无符号进行的,模2 32,和它产生两个表达式的所有值相同的定义的结果ab

  • 相反,如果类型ab小于int,两者都提升到int和使用符号算术,其中溢出所调用未定义的行为进行计算。

    • 如果int具有至少33个值位,则以上两个表达式均不会溢出,因此结果得到了完美定义,并且两个表达式具有相同的值。

    • 如果int恰好具有32个值位,则两个表达式(例如值)的计算都可能溢出,并且会导致两个表达式都溢出。为了避免这种情况,您需要编写。a=0xFFFFFFFFb=1((a & 255) + (b & 255)) & 255

  • 好消息是没有这样的平台1


1更确切地说,不存在这样的真实平台,但是可以配置DS9K以表现出这种行为并仍然符合C标准。


3
您的第二个子项目要求(1)a小于int(2)int具有32个值位(3)a=0xFFFFFFFF。那些不可能都是真的。
巴里

1
@Barry:一种似乎满足要求的情况是33位int,其中有32个值位和一个符号位。
Ben Voigt

2

相同,假定没有溢出。两种版本都不能真正避免溢出,但double和version对此具有更高的抵抗力。我不知道在这种情况下出现溢出问题的系统,但是我可以看到作者在有这种情况的情况下这样做。


1
指定的OP :(a和b是32位无符号整数)。除非int宽度为33位,否则即使发生溢出,结果也是相同的。无符号算术保证了这一点:不能用结果无符号整数类型表示的结果的模数要比可以用结果类型表示的最大值大1的模数减少。
chqrlie

2

是的,您可以用算术证明,但是有一个更直观的答案。

在添加时,每一位只会影响比其自身更重要的那些;从来没有那么重要。

因此,只要您只保留比修改的最低位低的有效位,那么对加法之前的高位进行的任何操作都不会改变结果。


0

证明是微不足道的,留给读者练习

但是要真正将其合法化为答案,您的第一行代码说取b** 的最后8位(所有高位均b设置为零)并将其添加到a,然后仅将结果设置的最后8位全部取高。位为零。

第二行说添加ab并取最后8位,所有高位为零。

结果中仅后8位有效。因此,仅后8位在输入中有效。

** 最后8位 = 8 LSB

同样有趣的是,输出将等于

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

如上所述,只有8个LSB是有效的,但结果是a unsigned int,而所有其他位均为零。该a + b会溢出,产生预期的结果。


不,它不会。字符数学的发生是因为int和char可以被签名。
Antti Haapala
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.