为什么(%256)与(a&0xFF)不同?


145

我一直认为执行(a % 256)优化程序时自然会使用高效的按位运算,就像我写的那样(a & 0xFF)

在编译器资源管理器gcc-6.2(-O3)上进行测试时:

// Type your code here, or load an example.
int mod(int num) {
    return num % 256;
}

mod(int):
    mov     edx, edi
    sar     edx, 31
    shr     edx, 24
    lea     eax, [rdi+rdx]
    movzx   eax, al
    sub     eax, edx
    ret

当尝试其他代码时:

// Type your code here, or load an example.
int mod(int num) {
    return num & 0xFF;
}

mod(int):
    movzx   eax, dil
    ret

似乎我完全错过了一些东西。有任何想法吗?


64
0xFF的是255不是256
瑞诗凯诗Raje

186
@RishikeshRaje:是吗?%也不&是。
usr2564301

27
@RishikeshRaje:我确信OP非常了解这一点。它们用于不同的操作。
干杯和健康。-Alf

28
如果num没有兴趣,您会得到更好的结果unsigned吗?
拔示巴

20
@RishikeshRaje按位和0xFF等效于无符号整数的模2 ^ 8。
2501年

Answers:


230

这是不一样的。尝试使用num = -79,这两种操作都会得到不同的结果。(-79) % 256 = -79,虽然(-79) & 0xff是一些正数。

使用unsigned int,操作相同,代码也可能相同。

PS-有人评论

它们不应相同,a % b定义为a - b * floor (a / b)

这不是在C,C ++,Objective-C(即问题中的代码将在其中编译的所有语言)中定义的方式。


评论不作进一步讨论;此对话已转移至聊天
马丁·彼得

52

简短答案

-1 % 256产生-1,而不是255它是-1 & 0xFF。因此,优化将是不正确的。

长答案

C ++遵循惯例(a/b)*b + a%b == a,这似乎很自然。a/b总是返回不带小数部分的运算结果(截断为0)。结果,a%b具有等于a或为0的符号。

分割-1/256产率0,并因此-1%256必须是-1为了满足上述条件((-1%256)*256 + -1%256 == -1)。这显然是不同的-1&0xFF这是0xFF。因此,编译器无法优化所需的方式。

N4606开始C ++标准[expr.mul§4]中的相关部分指出:

对于整数操作数,/运算符得出除掉任何小数部分的代数商。如果商a/b在结果类型中可表示,(a/b)*b + a%b则等于a[...]。

启用优化

但是,使用unsigned类型,优化将完全正确,满足上述约定:

unsigned(-1)%256 == 0xFF

另请参见

其他语言

您可以在Wikipedia上查找,这在不同的编程语言中的处理方式非常不同。


50

从C ++ 11开始,num % 256如果num为负,则必须为非正。

因此,位模式将取决于系统上有符号类型的实现:对于第一个负数的参数,结果不是提取最低有效8位。

如果num您的情况是unsigned这样,那将是另一回事:如今,我几乎希望编译器做出您引用的优化。


6
几乎,但不完全是。如果num为负,则num % 256为零或负(也称为非正)。
纳希希(Nayuki)

5
哪个IMO是标准中的错误:数学取模运算应采用除数的符号,在这种情况下为256。为了理解为什么要考虑这一点(-250+256)%256==6,但(-250%256)+(256%256)根据标准,它必须是“非肯定的”,因此不是6。像这样破坏关联性会带来现实生活的副作用:例如,当在整数坐标中计算“缩小”渲染时,必须将图像移位以使所有坐标均为非负值。
迈克尔

2
@Michael Modulus从未分配过加法(“ associative”是该属性的错误名称!),即使您对字母进行数学定义也是如此。例如,(128+128)%256==0但是(128%256)+(128%256)==256。也许对指定的行为有很好的异议,但是我不清楚这是您所说的。
丹尼尔·瓦格纳

1
@DanielWagner,您是对的,当然,我误解为“ associative”。但是,如果保留除数的符号并以模块化算术计算所有内容,则分布属性确实成立;在您的示例中,您将拥有256==0。关键是N在模N算术中具有完全可能的值,只有在所有结果都在范围内时0,...,(N-1),才有可能-(N-1),...,(N-1)
迈克尔

6
@Michael:除了%不是模运算符,它是余数运算符。
乔伦

11

我对编译器的推理没有心灵感应,但在这种情况下,%有必要处理负值(除法舍入为零),而&结果始终是低8位。

sar我看来,该指令听起来像“右移算术”,用符号位值填充空出的位。


0

从数学上讲,模的定义如下:

a%b = a-b *底数(a / b)

这里的权利应该为您清除它。我们可以消除整数的下限,因为整数除法等于下限(a / b)。但是,如果编译器使用您建议的通用技巧,则它必须适用于所有a和所有b。不幸的是,事实并非如此。从数学上讲,您的技巧对于无符号整数是100%正确的(我看到一个答案指出有符号整数坏了,但我可以确认也不否认,因为-a%b应该是正数)。但是,您能为所有b做到这一点吗?可能不是。这就是为什么编译器不这样做。毕竟,如果将模简单地写成一个按位运算,那么我们只需添加一个模电路,如加法和其他运算。


4
我认为您将“地板”与“截断”混淆了。早期的计算机使用截断除法,因为即使在情况发生均分的情况下,它通常也比底除法更容易计算。我很少见到截断分隔比底数分隔有用的情况,但是许多语言遵循FORTRAN的使用截断分隔的用法。
超级猫

@supercat从数学上讲,地板截断的。它们都有相同的效果。它们在计算机中的实现方式可能不尽相同,但它们执行相同的操作。
user64742 '16

5
@TheGreatDuck:负数不一样。的下限-2.3-3,而如果将其截断-2.3为整数,则会得到-2。请参阅en.wikipedia.org/wiki/Truncation。“对于负数,截断不会在与底函数相同的方向上四舍五入”。而%负数的行为恰恰是OP看到上述行为的原因。
Mark Dickinson

@MarkDickinson我很确定c ++中的模数可以为正除数提供正值,但是我不会争论。
user64742 '16

1
@TheGreatDuck-参见示例:cpp.sh/3g7h (请注意,C ++ 98并未定义使用了两种可能的变体中的哪一种,但是使用了最新的标准,因此您可能使用了C ++的实现过去的做法有所不同...)
Periata Breatta
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.