看起来整个循环的目的是将 32 位 arg 中的所有位异或。即计算parity。
从最后一条指令 (and $1,%eax) 开始,我们知道只有结果的低位很重要。
考虑到这一点,xor %edx,%eax 变得更加清晰:将%edx 的当前低位异或到%eax。高垃圾无所谓。
shr 循环直到x 的所有位都被移出。我们总是可以循环 32 次以获取所有位,但这比在 x 为 0 时停止效率要低。(由于 XOR 的工作原理,我们不需要对 0 位进行实际的 XOR;那没有效果。)
一旦我们知道函数的作用,填写 C 就变成了巧妙/紧凑的 C 语法的练习。起初我认为y ^= (x>>=1); 会适合循环,但在第一次使用它之前,x 会发生变化。
我看到在一个 C 语句中执行此操作的唯一方法是使用 , 运算符(它确实引入了 sequence point,因此在左侧阅读 x 并在右侧修改它是安全的,)。所以,y ^= x, x>>=1; 适合。
或者,为了更易读的代码,只是作弊并将两个语句与;放在同一行。
int f1(unsigned x) {
int y = 0;
while(x != 0) {
y ^= x; x>>=1;
}
return y & 1;
}
使用gcc5.3 -O3 on the Godbolt compiler explorer,编译为与问题中所示基本相同的asm。问题的代码de-optimizes the xor-zeroing idiom 改为mov $0, %eax,并优化了gcc 对ret 指令的愚蠢重复。 (或者可能使用了没有这样做的早期版本的 gcc。)
循环非常低效:这是一种有效的方式:
我们不需要复杂度为 O(n) 的循环(其中 n 是 x 的位宽度)。相反,我们可以获得 O(log2(n)) 复杂度,并且实际上利用 x86 技巧只执行前 2 步。
对于由寄存器确定的指令,我省略了操作数大小的后缀。 (xorw 使 16 位异或显式除外。)
#untested
parity:
# no frame-pointer boilerplate
xor %eax,%eax # zero eax (so the upper 24 bits of the int return value are zeroed). And yes, this is more efficient than mov $0, %eax
# so when we set %al later, the whole of %eax will be good.
movzwl 4(%esp), %edx # load low 16 bits of `x`. (zero-extend into the full %edx is for efficiency. movw 4(%esp), %dx would work too.
xorw 6(%esp), %dx # xor the high 16 bits of `x`
# Two loads instead of a load + copy + shift is probably a win, because cache is fast.
xor %dh, %dl # xor the two 8 bit halves, setting PF according to the result
setnp %al # get the inverse of the CPU's parity flag. Remember that the rest of %eax is already zero, so the result is already zero-extended to 32-bits (int return value)
ret
是的,没错,x86 has a parity flag (PF) 是从“根据结果设置标志”的每条指令的结果的低 8 位更新的,例如 xor。
我们使用np 条件,因为PF = 1 表示偶校验:所有位的异或 = 0。我们需要逆返回 0 以获得偶校验。
为了利用它,我们通过将高半部分降低到低半部分并组合,重复两次以将 32 位减少到 8 位来进行 SIMD 样式的水平缩减。
在设置标志的指令之前将 eax 归零(使用 xor)比设置标志/setp %al/movzbl %al, %eax 更有效,正如我在What is the best way to set a register to zero in x86 assembly: xor, mov or and? 中解释的那样。
或者,正如@EOF所指出的,如果CPUID为POPCNT feature bit is set,则可以使用popcnt并测试低位以查看设置的位数是偶数还是奇数。 (另一种看待这个问题的方式:xor 是无进位的加法运算,因此无论是对所有位进行异或或将所有位水平相加,低位都是相同的)。
GNU C 也有__builtin_parity 和__builtin_popcnt,如果你告诉编译器编译目标支持它(使用-march=... 或-mpopcnt),它们使用硬件指令,但否则编译为一个有效的序列目标机。 Intel 内部函数始终编译为机器指令,而不是回退序列,并且在没有适当的 -mpopcnt 目标选项的情况下使用它们是编译时错误。
不幸的是,gcc 没有将纯 C 循环识别为奇偶校验计算并将其优化为这一点。一些编译器(如 clang 和可能的 gcc)可以识别某些类型的 popcount 惯用语,并将它们优化为 popcnt 指令,但在这种情况下不会发生这种模式识别。 :(
See these on godbolt.
int parity_gnuc(unsigned x) {
return __builtin_parity(x);
}
# with -mpopcnt, compiles the same as below
# without popcnt, compiles to the same upper/lower half XOR algorithm I used, and a setnp
# using one load and mov/shift for the 32->16 step, and still %dh, %dl for the 16->8 step.
#ifdef __POPCNT__
#include <immintrin.h>
int parity_popcnt(unsigned x) {
return _mm_popcnt_u32(x) & 1;
}
#endif
# gcc does compile this to the optimal code:
popcnt 4(%esp), %eax
and $1, %eax
ret
另请参阅x86 标签 wiki 中的其他链接。