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

Mar*_*tin 92 c++ binary logic

我正在浏览一些C++代码,发现这样的东西:

(a + (b & 255)) & 255
Run Code Online (Sandbox Code Playgroud)

双重和我生气,所以我想到:

(a + b) & 255
Run Code Online (Sandbox Code Playgroud)

(a并且b是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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

另外,抱歉Lisp-y标题.随意编辑它.

Bat*_*eba 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

完成证明.

  • 它是数学证明,忽略了溢出的可能性.考虑`A = 0xFFFFFFFF,B = 1,C = 3`.第一个身份不成立.(对于无符号算术,溢出不会成为问题,但它有点不同.) (81认同)
  • @JackAidley:当正确完成时它们是合适的*(由于忽略考虑溢出而不是一个). (25认同)
  • 诸如此类的数学证明不适用于证明计算机体系结构上的整数行为. (17认同)
  • 实际上,`(a +(b&255))&255`与`(a +(b%256))%N%256`相同,其中`N`比最大无符号值大1.(后一个公式意味着被解释为数学整数的算术) (4认同)
  • @Shaz:测试脚本确实如此,但不是问题的一部分. (3认同)
  • -1因为证明是错误的.考虑到`a + b`是以2 <sup> 32 </ sup>为模计算的,可以正确地写出来. (3认同)
  • @AlexD使用AND作为模数的技巧仅在C是2的幂时才起作用.你会发现,对于C的有效值,即使在溢出的情况下,身份仍然存在. (2认同)

plu*_*ash 21

在位置加法,减法和乘法中,输入的更多有效数字不会影响结果的较低有效位.这适用于二进制算术,与十进制算术一样多.

但是,从二进制算术中获取规则并将它们应用于C时我们必须要小心(我相信C++在这个问题上与C具有相同的规则,但我不是100%肯定)因为C算术有一些可以使我们感到震惊的神秘规则起来.C中的无符号算术遵循简单的二进制环绕规则,但带符号的算术溢出是未定义的行为.更糟糕的是,在某些情况下,C会自动将"无符号"类型"提升"为(signed)int.

C中未定义的行为可能特别隐蔽.根据您对二进制算术的理解,一个愚蠢的编译器(或低优化级别的编译器)可能会按照您的预期执行操作,而优化编译器可能会以奇怪的方式破坏您的代码.


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

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

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

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

OTOH if ab者是有符号整数,其大小大于或等于int的大小,那么即使在二进制补码实现中,也有一种语句定义明确而另一种语句是未定义行为的情况.


Bar*_*rry 20

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

无符号a可以改写为m * 0x100 + b一些未签名的m,b,0 <= b < 0xff,0 <= 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
Run Code Online (Sandbox Code Playgroud)

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


其他整数类型怎么样?

  • 对于64位无符号整数,所有上述同样也适用,只是代替2^642^32.
  • 对于8位和16位无符号整数,添加涉及促销int.int在任何这些操作中,这肯定既不会溢出也不会消极,因此它们都保持有效.
  • 对于签署整数,如果有一个a+ba+(b&255)溢出,这是不确定的行为.所以平等不能成立 - 有些情况(a+b)&255是未定义的行为,但事实(a+(b&255))&255并非如此.


ala*_*ain 17

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

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


上述情况并非总是如此,C++标准允许实现破坏这一点.

如果OP意味着"32位无符号整数" ,那么这样的Deathstation 9000 :- )必须使用33位.如果是这样的话,DS9K必须使用32位,32位和填充位.(无符号整数必须与符合§3.9.1/ 3的有符号整数具有相同的大小,并且在§3.9.1/ 1中允许填充位.)其他大小和填充位组合也可以工作.intunsigned shortunsigned intintunsigned int

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

  • 整数表示必须使用"纯二进制"编码方案(§3.9.1/ 7和脚注),除填充位和符号位之外的所有位必须提供2 n的值
  • 仅当int可以表示源类型的所有值(§4.5/ 1)时才允许int promote,因此int必须至少有32位对该值有贡献,加上符号位.
  • int不能有更多的值的比特(不包括符号位)超过32,因为其他的加成不能溢出.

  • 除了添加之外还有许多其他操作,其中高位垃圾不会影响您感兴趣的低位结果.请参阅[此问答大约2的补码](http://stackoverflow.com/questions/34377711/哪个-ss-complement-integer-operations-can-used-without-zeroing-high-bits-in),它使用x86 asm作为用例,但在任何情况下也适用于无符号二进制整数. (2认同)
  • 虽然每个人都有权匿名进行投票,但我总是将评论视为学习的机会. (2认同)
  • 这是迄今为止最容易理解的答案/理由,IMO.进位/借位加/减仅以二进制方式从低位到高位(从右到左)传播,与十进制相同.IDK为什么有人会这样做. (2认同)
  • @Bathsheba:是的,幸运的是C-as-portable-assembly-language确实主要适用于无符号类型.即使是故意恶意的C实现也不能打破这一点.它只是签名类型,在C中真正可移植的bit-hack的东西是可怕的,而Deathstation 9000可以真正打破你的代码. (2认同)

Mat*_* M. 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;
}
Run Code Online (Sandbox Code Playgroud)

此枚举通过的所有可能值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;
}
Run Code Online (Sandbox Code Playgroud)

再一次,据Clang说:平等成立.

好吧,你去:)


1 当然,如果某个程序无意中触发了未定义的行为,那么它就不会有太大的影响.

  • 既然你不能评估所有(4294967296 - 1)*(4294967296 - 1)可能的结果,你会以某种方式减少吗?我的意见MAX应该是(4294967296 - 1),如果你这样做,但它永远不会像我说的那样在我们的一生中完成......所以,毕竟我们不能在实验中表现出平等,至少不是像你这样的人描述. (2认同)

chq*_*lie 5

简单的回答是:两个表达式是等价的

  • 由于ab是 32 位无符号整数,因此即使在溢出的情况下结果也是相同的。无符号算术保证了这一点:无法由结果无符号整数类型表示的结果会以比结果类型可以表示的最大值大 1 的数为模进行缩减。

长的答案是:没有已知的平台会导致这些表达方式有所不同,但由于积分提升的规则,标准并不保证这一点。

  • a如果和的类型b(无符号 32 位整数)的秩高于int,则计算将以无符号、模 2 32的形式执行,并且对于a和的所有值,两个表达式都会产生相同的定义结果b

  • a相反,如果和 的类型b小于int,则两者都会提升为int并使用有符号算术执行计算,其中溢出会调用未定义的行为。

    • 如果int至少有 33 个值位,则上述两个表达式都不会溢出,因此结果是完美定义的,并且两个表达式的值相同。

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

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


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

  • 您的第二个子项目要求 (1) `a` 小于 `int` (2) `int` 有 32 个值位 (3) `a=0xFFFFFFFF`。这些不可能都是真的。 (3认同)
  • @Barry:似乎满足要求的一种情况是 33 位“int”,其中有 32 个值位和 1 个符号位。 (2认同)