看起来整个循环的目的是将 32 位参数中的所有位异或在一起。即计算parity.
从最后一条指令开始向后工作(and $1,%eax
),我们知道只有结果的低位很重要。
考虑到这一点,xor %edx,%eax
变得更清楚:异或当前的低位%edx
into %eax
。垃圾高了也没关系。
The shr
循环直到所有x
的位已被移出。我们总是可以循环 32 次来获取所有位,但这比停止一次效率低x
是 0。(由于 XOR 的工作原理,我们不需要在 0 位中进行实际的 XOR;这没有效果。)
一旦我们知道了函数的作用,填充 C 就变成了巧妙/紧凑的 C 语法的练习。我一开始以为y ^= (x>>=1);
适合循环内部,但这会改变x
before第一次使用它。
我认为在一个 C 语句中做到这一点的唯一方法是使用,
运算符(它确实引入了序列点,所以阅读是安全的x
左侧并修改a的右侧,
). So, y ^= x, x>>=1;
fits.
或者,为了获得更具可读性的代码,只需作弊并将两个语句放在同一行上;
.
int f1(unsigned x) {
int y = 0;
while(x != 0) {
y ^= x; x>>=1;
}
return y & 1;
}
这编译为与问题中所示基本相同的 asm, using Godbolt 编译器浏览器上的 gcc5.3 -O3。问题的代码反优化异或归零习惯用法 to a 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 有一个奇偶校验标志(PF)它是从“根据结果设置标志”的每条指令的结果的低 8 位更新的,例如xor.
我们使用np
条件因为PF
= 1 表示偶校验:所有位的异或 = 0。对于偶校验,我们需要求逆来返回 0。
为了利用它,我们进行了 SIMD 式的水平缩减,将高半部分降低到低半部分并组合,重复两次以将 32 位缩减为 8 位。
在设置标志的指令之前将 eax 归零(使用异或)比设置标志/更有效setp %al
/ movzbl %al, %eax
,正如我在在 x86 汇编中将寄存器设置为零的最佳方法是什么:xor、mov 或 and?.
或者,正如 @EOF 指出的,如果 CPUIDPOPCNT功能位已设置,可以使用popcnt测试低位,看看设置的位数是偶数还是奇数。 (另一种看待这个问题的方法是:异或是不带进位的加法,因此无论您将所有位异或在一起还是将所有位水平相加,低位都是相同的)。
GNU C 也有__builtin_parity
and __builtin_popcnt
如果您告诉编译器编译目标支持它,则使用硬件指令(使用-march=...
or -mpopcnt
),但否则编译为目标机器的有效序列。英特尔内在函数始终编译为机器指令,而不是后备序列,并且在没有适当的情况下使用它们会导致编译时错误-mpopcnt
目标选项。
不幸的是,gcc 无法将纯 C 循环识别为奇偶校验计算并将其优化为此。一些编译器(例如 clang 和可能的 gcc)可以识别某些类型的 popcount 习惯用法,并将它们优化为popcnt
指令,但在这种情况下不会发生这种模式识别。 :(
在 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标签维基。