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

Is ((a + (b & 255)) & 255) the same as ((a + b) & 255)?

本文关键字:相同 是否      更新时间:2023-10-16

我正在浏览一些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;
    }
}

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

另外,很抱歉lisp-y标题。请随时编辑。

它们是相同的。这是一个证明:

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

完成证明。

引理:无符号aa & 255 == a % 256

无符号的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不是。

在位置添加,无符号数字的减法和乘法中以产生无签名的结果,输入的更重要的数字不会影响不太重要的数字结果。这与十进制算术一样多地适用于二元算术。它也适用于"两次补体"签名的算术,但不适合符号签名算术。

但是,在从二进制算术中获取规则并将其应用于c时,我们必须小心(我相信C 在此内容上的规则与C具有相同的规则,但我不是100%确定),因为C算术具有一些神秘的规则可以绊倒我们。C中的未签名算术遵循简单的二进制环绕规则,但签名的算术溢出是未定义的行为。在某些情况下,更糟糕的是,C会自动"促进"未签名类型的(签名)int。

C中不确定的行为可能是特别不可分割的。愚蠢的编译器(或较低优化级别的编译器)可能会根据对二进制算术的理解来完成您的期望,而优化编译器可能会以奇怪的方式破坏您的代码。


因此,在问题中回到了该公式取决于操作数类型。

如果它们是大小大于或等于int大小的未签名整数,则加法操作员的溢出行为被很好地定义为简单的二进制自由度。在加法操作之前,我们是否掩盖了一个操作数的高24位,对结果的低位没有影响。

如果它们是大小小于 int的未签名整数,则将晋升为(签名)int。签名整数的溢出是不确定的行为,但至少在我遇到的每个平台上,不同整数类型之间的大小差异足够大,以至于单个添加两个促进值不会导致溢出。因此,我们再次可以回到简单的二进制算术论点以认为等同的陈述。

如果它们是签名的整数,其大小小于int的整数,那么再次溢出就不会发生,并且在两项组合实现中,我们可以依靠标准的二进制算术论点来说它们是等效的。关于符号标志或补充实现的实现,它们将不可行。

OTOH如果ab是签名的整数,其大小大于或等于int的大小,那么即使在TWOS补体实现的情况下,在某些情况下,在某些情况下,一个语句将得到很好的定义,而另一个语句是不确定的行为。

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

还记得在学校加入吗?您可以通过数字添加数字数字,并为下一个数字列添加一个携带值。以后的数字列无法影响已经处理过的列。因此,如果您仅在结果中或在参数中首先将数字归零,则不会有所作为。


以上并不总是正确的,C 标准允许实现会破坏此问题。

这样的死亡站9000 :-)必须使用33位int,如果OP表示具有" 32位无签名整数"的unsigned short。如果unsigned int的意思是,DS9K将必须使用32位int,以及带有填充位的32位unsigned int。(要求未签名的整数具有与签名的尺寸相同的大小与第3.9.1/3节相同,并且第§3.9.1/1/1中允许使用填充位。)其他尺寸和填充位的组合也可以工作。<<<<<<<<<<<<<<<<<<<</p> 据我所知,这是打破它的唯一方法,因为:

  • 整数表示必须使用"纯二进制"编码方案(第3.9.1/7节和脚注),除填充位和符号位以外的所有位都必须贡献2 n 的值
  • 仅当int可以代表源类型的所有值(§4.5/1)时,才允许int促销,因此int必须至少具有32位有助于该值的位,再加上标志位。
  • int不能比32更重要的值位(不计数标志位),因为否则添加剂不会溢出。

您已经有了明智的答案:无符号算术是模量算术,因此结果将成立,您可以数学上证明它...


关于计算机的一件很酷的事情是计算机很快。确实,它们是如此之快,以至于在合理的时间内可以列举所有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 holdsn";
    return 0;
}

这将通过32位空间中的ab的所有可能值进行枚举,并检查平等是否保持。如果不这样做,它将打印不起作用的情况,您可以用作理智检查。

,根据clang: equality持有

此外,鉴于算术规则是位宽度的不可知论(在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 holdsn";
    return 0;
}

再次,根据clang: equality保持

好吧,你去了:)


1 当然,如果程序无意中触发了未定义的行为,则不会证明太多。

快速答案是:两个表达式均等

  • 由于ab是32位未签名的整数,因此即使在溢出的情况下,结果也相同。未签名的算术可以保证以下:一个无法用结果的无符号整数类型表示的结果,减少了模型,该数字大于最大值的数字。

漫长的答案是:没有已知的平台在这些平台上会有所不同,但是由于整体促销的规则,标准并不能保证它。

  • 如果ab的类型(未签名的32位整数)的排名高于int,则该计算是无签名的,Modulo 2 32 ,并且可以产生相同定义的定义ab的所有值的两个表达式的结果。

  • 相反,如果ab的类型小于int,则两者都会促进到int,并且使用符号算术进行计算,其中溢出调用不确定的行为。

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

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

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


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

相同的假设没有溢出。这两种版本都不对溢出真正免疫,但双重版本和版本对其更具耐药性。我不知道在这种情况下溢出的系统,但是我可以看到作者这样做。您可以用算术证明这一点,但是有一个更直观的答案。

添加时,每一点只会影响比自身更重要的人。从来没有那么重要的人。

因此,在添加之前对较高位做的任何事情都不会改变结果,只要您只保持比最低的钻头修改的重要性。

证明是微不足道的,作为读者的练习

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

第二行说添加 ab,然后将所有较高位零位的最后8位占据。

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

** 最后8位 = 8 LSB

还有趣的是,输出将等效于

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

如上所述,只有8个LSB很重要,但结果是所有其他位零的unsigned inta + b将溢出,产生预期的结果。

相关文章: