((a + (b & 255)) & 255) 和 ((a + b) & 255) 一样吗?
Posted
技术标签:
【中文标题】((a + (b & 255)) & 255) 和 ((a + b) & 255) 一样吗?【英文标题】:Is ((a + (b & 255)) & 255) the same as ((a + b) & 255)? 【发布时间】:2017-04-06 16:58:04 【问题描述】:我正在浏览一些 C++ 代码,发现如下内容:
(a + (b & 255)) & 255
双重AND惹恼了我,所以我想到了:
(a + b) & 255
(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;
虽然脚本证实了我的假设(两个操作相等),但我仍然不相信它,因为 1) random 和 2) 我不是数学家,I have no idea what am I doing。
另外,对于 Lisp-y 标题感到抱歉。随意编辑它。
【问题讨论】:
那个脚本是什么语言的?Math.random()
是否在 [0,1) 上返回整数或双精度数?我不认为你的剧本(我能说的最好)反映了你提出的问题。
什么是c/c++代码?它们是不同的语言。
您无法重现您尝试在 JS 中测试的行为。这就是为什么在语言选择上每个人都只有你。 JS 不是强类型的,答案主要取决于 C/C++ 中变量的类型。考虑到您提出的问题,JS 完全是一派胡言。
@WeatherVane 这是基本的伪代码,使用 javascript 函数名称。他的问题是关于 &
和 +
在 C 和 C++ 中对无符号整数的行为。
请记住,“我编写了一个测试程序并得到了我对所有可能输入的预期答案”实际上并不能保证某些东西的行为符合您的预期。未定义的行为可能会像那样令人讨厌;只有在你说服自己你的代码是正确的之后才会给出意想不到的结果。
【参考方案1】:
它们是一样的。这是一个证明:
先记下身份(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
。第一个恒等式不成立。 (溢出不会成为无符号算术的问题,但它有点不同。)
其实(a + (b & 255)) & 255
和(a + (b % 256)) % N % 256
是一样的,其中N
比最大无符号值大一。 (后一个公式被解释为数学整数的算术)
诸如此类的数学证明不适用于证明整数在计算机体系结构上的行为。
@JackAidley:它们是合适的
@Shaz:测试脚本确实如此,但不是问题的一部分。【参考方案2】:
是的,(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 节和脚注),除填充位和符号位之外的所有位必须贡献值 2n 仅当int
可以表示源类型的所有值(第 4.5/1 节)时才允许进行 int 提升,因此int
必须至少有 32 位参与该值,外加一个符号位。李>
int
的值位(不包括符号位)不能超过 32 个,否则加法不会溢出。
【讨论】:
除了加法之外,还有许多其他操作,其中高位中的垃圾不会影响您感兴趣的低位中的结果。请参阅this Q&A about 2's complement,它使用 x86 asm 作为使用-情况,但也适用于任何情况下的无符号二进制整数。 虽然每个人都有匿名投票的权利,但我始终感谢评论是一个学习的机会。 这是迄今为止最容易理解的答案/论据,IMO。加法/减法中的进位/借位仅在二进制中从低位传播到高位(从右到左),与十进制相同。 IDK 为什么有人会对此投反对票。 @Bathsheba: CHAR_BIT 不需要为 8。但 C 和 C++ 中的无符号类型需要表现为具有一定位宽的普通 base2 二进制整数。我认为这要求 UINT_MAX 是2^N-1
。 (N 甚至可能不需要是 CHAR_BIT 的倍数,我忘记了,但我很确定标准要求环绕发生以 2 的某个幂为模。)我认为你可以获得怪异的唯一方法是通过提升到签名类型,其宽度足以容纳 a
或 b
,但在所有情况下都不足以容纳 a+b
。
@Bathsheba:是的,幸运的是,C-as-portable-assembly-language 确实主要适用于无符号类型。即使是故意敌对的 C 实现也无法打破这一点。只有签名类型对于真正可移植的 C 中的位黑客来说是可怕的,而 Deathstation 9000 真的可以破坏你的代码。【参考方案3】:
在无符号数的位置加法、减法和乘法以产生无符号结果时,输入的较高有效数字不会影响结果的较低有效数字。这适用于二进制算术,就像它适用于十进制算术一样。它也适用于“二进制补码”有符号算术,但不适用于符号幅度有符号算术。
但是,在从二进制算术中获取规则并将它们应用于 C 时,我们必须小心(我相信 C++ 在这些东西上与 C 有相同的规则,但我不是 100% 确定),因为 C 算术有一些神秘的规则可以绊倒我们。 C 中的无符号算术遵循简单的二进制环绕规则,但有符号算术溢出是未定义的行为。更糟糕的是,在某些情况下,C 会自动将无符号类型“提升”为 (signed) int。
C 中未定义的行为可能特别隐蔽。愚蠢的编译器(或低优化级别的编译器)可能会根据您对二进制算术的理解执行您期望的操作,而优化的编译器可能会以奇怪的方式破坏您的代码。
所以回到问题中的公式,等价取决于操作数类型。
如果它们是大小大于或等于int
的大小的无符号整数,则加法运算符的溢出行为被明确定义为简单的二进制回绕。我们是否在加法运算之前屏蔽掉一个操作数的高 24 位对结果的低位没有影响。
如果它们是大小小于int
的无符号整数,那么它们将被提升为(有符号)int
。有符号整数的溢出是未定义的行为,但至少在我遇到的每个平台上,不同整数类型之间的大小差异足够大,以至于两个提升值的单次相加不会导致溢出。所以我们可以再次回到简单的二进制算术参数来认为语句等效。
如果它们是大小小于 int 的有符号整数,则不会再次发生溢出,并且在二进制补码实现中,我们可以依靠标准二进制算术参数来说明它们是等价的。在符号大小或补充实现上,它们不会是等价的。
OTOH 如果 a
和 b
是大小大于或等于 int 大小的有符号整数,那么即使在二进制补码实现中,也存在一个语句定义明确而另一个语句未定义的情况行为。
【讨论】:
【参考方案4】:相同假设没有溢出。这两个版本都不能真正避免溢出,但 double 和版本更能抵抗溢出。我不知道在这种情况下溢出是一个问题的系统,但我可以看到作者这样做,以防万一。
【讨论】:
指定的 OP:(a 和 b 是 32 位无符号整数)。除非int
是 33 位宽,否则结果是相同的 even 以防溢出。无符号算术保证了这一点:一个不能由产生的无符号整数类型表示的结果以比结果类型可以表示的最大值大一的数字为模减少。【参考方案5】:
您已经有了聪明的答案:无符号算术是模算术,因此结果将成立,您可以用数学方法证明...
不过,关于计算机的一个很酷的事情是计算机速度很快。事实上,它们的速度如此之快,以至于可以在合理的时间内枚举所有 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 位空间中 a
和 b
的所有可能值,并检查相等性是否成立。如果没有,它会打印不起作用的案例,您可以将其用作健全性检查。
并且,according to 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;
再一次,according to Clang:平等成立。
好吧,你去吧:)
1当然,如果一个程序无意中触发了未定义行为,那也证明不了多少。
【讨论】:
您说使用 32 位值很容易,但实际上使用 16 位...:D @WilliMentzel:这句话很有趣。我最初想说的是,如果它适用于 16 位,那么它将适用于 32 位、64 位和 128 位,因为标准没有针对不同位宽的特定行为……但我记得它确实有对于小于int
的位宽:首先将小整数转换为int
(一个奇怪的规则)。所以我实际上必须用 32 位进行演示(然后扩展到 64 位、128 位……)。
既然你不能评估所有 (4294967296 - 1) * (4294967296 - 1) 可能的结果,你会以某种方式减少吗?我认为 MAX 应该是 (4294967296 - 1) 如果你这样做,但它永远不会像你说的那样在我们的一生中完成......所以,毕竟我们不能在实验中证明平等,至少在像你这样的实验中不能描述。
在一个 2 的补码实现上测试它并不能证明它可以移植到符号幅度或具有 Deathstation 9000 类型宽度的一个补码。例如窄的无符号类型可以提升为 17 位 int
,它可以表示所有可能的 uint16_t
,但其中 a+b
可能会溢出。这只是比int
更窄的无符号类型的问题; C requires that unsigned
types are binary integers, so wraparound happens modulo a power of 2
同意 C 因其自身的利益而过于便携。如果他们将 2 的补码、有符号的算术右移以及一种使用包装语义而不是未定义行为语义来进行有符号算术的方法标准化,那将是真的,对于那些当你想要包装。然后 C 可以再次用作便携式汇编程序,而不是雷区,这要归功于现代优化编译器,这使得留下任何未定义的行为(至少对于您的目标平台)是不安全的。仅在 Deathstation 9000 实现上的未定义行为是可以的,因为您指出)。【参考方案6】:
快速回答是:两个表达式是等价的
由于a
和b
是32 位无符号整数,即使发生溢出,结果也是相同的。无符号算术保证了这一点:不能用得到的无符号整数类型表示的结果以比结果类型可以表示的最大值大一的数字为模减少。
长答案是:没有已知的平台在这些表达方式上会有所不同,但标准不保证这一点,因为积分提升的规则。
如果 a
和 b
(无符号 32 位整数)的类型比 int
的等级更高,则计算为无符号,模 232,对于a
和b
的所有值,这两个表达式都会产生相同的定义结果。
相反,如果a
和b
的类型小于int
,则两者都提升为int
,并使用有符号算术执行计算,其中溢出会调用未定义的行为。
如果int
至少有33个值位,以上表达式都不会溢出,所以结果定义完美,两个表达式的值相同。
如果 int
正好有 32 个值位,计算 可能 对 both 表达式溢出,例如值 a=0xFFFFFFFF
和 b=1
会导致两个表达式中的溢出。为了避免这种情况,你需要写((a & 255) + (b & 255)) & 255
。
好消息是没有这样的平台1。
1 更准确地说,不存在这样的真实平台,但可以配置DS9K 来展示这样的行为并仍然符合C 标准。
【讨论】:
您的第二个子项目符号要求 (1)a
小于 int
(2) int
具有 32 个值位 (3) a=0xFFFFFFFF
。这些不可能都是真的。
@Barry:似乎符合要求的一种情况是33位int
,其中有32个值位和1个符号位。【参考方案7】:
引理: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
所以是的,这是真的。对于 32 位无符号整数。
其他整数类型呢?
对于 64 位无符号整数,上述所有方法同样适用,只需将2^64
替换为 2^32
。
对于 8 位和 16 位无符号整数,加法涉及升级到 int
。这个int
在任何这些操作中绝对不会溢出或为负,所以它们都保持有效。
对于有符号整数,如果a+b
或a+(b&255)
溢出,这是未定义的行为。所以等式不成立——在某些情况下(a+b)&255
是未定义的行为,但(a+(b&255))&255
不是。
【讨论】:
【参考方案8】:是的,你可以用算术证明它,但有一个更直观的答案。
添加时,每一位只影响比自己重要的那些;从来没有那些不那么重要的。
因此,无论您在加法之前对高位做什么都不会改变结果,只要您只保留比修改的最低位低的位。
【讨论】:
【参考方案9】:证明是微不足道的,留给读者作为练习
但要真正将此作为答案合法化,您的第一行代码说取b
** 的最后 8 位(b
的所有高位设置为零)并将其添加到 a
然后只取结果的最后 8 位,将所有高位设置为零。
第二行表示将a
和b
相加,并取最后8 位,所有高位为零。
只有最后 8 位在结果中是有效的。因此,只有最后 8 位在输入中是重要的。
** 最后 8 位 = 8 LSB
另外有趣的是,输出将等同于
char a = something;
char b = something;
return (unsigned int)(a + b);
如上所述,只有 8 个 LSB 是有效的,但结果是 unsigned int
,所有其他位为零。 a + b
将溢出,产生预期的结果。
【讨论】:
不,不会。 Char 数学发生在 int 和 char 可以签名。以上是关于((a + (b & 255)) & 255) 和 ((a + b) & 255) 一样吗?的主要内容,如果未能解决你的问题,请参考以下文章
软考(20)-TCP&DNS&Telnet&DHCP&windows管理
软考(20)-TCP&DNS&Telnet&DHCP&windows管理