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

时间:2016-11-22 21:09:03

标签: c++ binary logic

我正在浏览一些C ++代码,并发现了类似这样的内容:

(a + (b & 255)) & 255

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

(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;
    }
}
&#13;
&#13;
&#13;

虽然剧本证实了我的假设(两个操作都是平等的),但我仍然不相信它,因为1)random和2)我不是数学家,{{3} }。

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

9 个答案:

答案 0 :(得分:77)

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

首先注意身份(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

完成证明。

答案 1 :(得分:20)

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

然而,我们在从二进制算术中取规则并将它们应用于C时必须要小心(我相信C ++在这个东西上有与C相同的规则,但我并非100%肯定)因为C算术有一些奥术可以惹恼我们的规则。 C中的无符号算术遵循简单的二进制环绕规则,但带符号的算术溢出是未定义的行为。更糟糕的是,在某些情况下,C会自动&#34;促进&#34;无符号类型到(signed)int。

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

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

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

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

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

OTOH如果ab是大小大于或等于int大小的有符号整数,那么即使在二进制补码实现上也存在一个语句定义明确的情况其他将是未定义的行为。

答案 2 :(得分: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^64代替2^32
  • 对于8位和16位无符号整数,添加涉及升级到int。在这些操作中,int肯定既不会溢出也不会消极,所以它们都保持有效。
  • 对于签名的整数,如果a+ba+(b&255)溢出,则表示未定义的行为。所以平等不能成立 - 有些情况(a+b)&255是未定义的行为但(a+(b&255))&255不是。{1}}。

答案 3 :(得分:17)

是的,(a + b) & 255没问题。

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

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

如果OP意味着int带有“32位”,那么这样的Deathstation 9000 : - )必须使用33位unsigned short无符号整数“。如果需要unsigned int,则DS9K必须使用32位int和带填充位的32位unsigned int。 (无符号整数必须与符合§3.9.1/ 3的有符号整数具有相同的大小,并且§3.9.1/ 1中允许使用填充位。)其他大小和填充位组合也可以工作。 / p>

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

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

答案 4 :(得分: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;
}

这将枚举32位空间中ab的所有可能值,并检查相等性是否成立。如果没有,则会打印不起作用的外壳,您可以将其用作完整性检查。

而且,according to Clang Equality成立

此外,假设算术规则是位宽度不可知的(高于int位宽度),则此等式将适用于32位或更多的无符号整数类型,包括64位和128位。 / p>

注意:编译器如何在合理的时间范围内枚举所有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;
}

再一次,according to Clang Equality持有

嗯,你去吧:)。

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

答案 5 :(得分:4)

快速回答是:两个表达式都是等价的

  • 由于ab是32位无符号整数,即使出现溢出,结果也是一样的。无符号算术保证这一点:无法用结果无符号整数类型表示的结果以模数的形式减少,该数字比结果类型可以表示的最大值大一个。

答案很长:没有已知的平台,这些表达方式会有所不同,但标准并不保证,因为整体推广的规则。

  • 如果ab(无符号32位整数)的类型具有比int更高的等级,则计算将以无符号形式执行,模2 32 ,它为ab的所有值的两个表达式生成相同的定义结果。

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

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

    • 如果int正好有32个值位,那么两个表达式的计算可以溢出,例如值a=0xFFFFFFFF和{ {1}}会导致两个表达式都出现溢出。为了避免这种情况,您需要编写b=1

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

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

答案 6 :(得分:2)

相同假设没有溢出。这两个版本都不会真正免受溢出的影响,但双版本和版本对它的抵抗力更强。我不知道一个系统,在这种情况下溢出是一个问题,但我可以看到作者这样做,以防有一个。

答案 7 :(得分:1)

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

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

因此,无论您在添加之前对高位执行的操作都不会更改结果,只要您保持位不如修改的最低位重要。

答案 8 :(得分:1)

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

但实际上要将此作为答案合法化,您的第一行代码表示将b **的最后8位(b的所有较高位设置为零)并将其添加到{ {1}}然后只将结果设置的最后8位设置为零。

第二行表示添加aa,并取最后8位,所有较高位为零。

结果中只有最后8位是重要的。因此,只有最后8位在输入中是重要的。

** 最后8位 = 8 LSB

另外有趣的是,输出将等同于

b

如上所述,只有8 LSB是重要的,但结果是char a = something; char b = something; return (unsigned int)(a + b); ,所有其他位为零。 unsigned int将溢出,产生预期结果。