所以我看到了类似的问题 toggle a bit at ith positon和How do you set, clear, and toggle a single bit?,但我想知道是否有一种很好的方法可以在x86-64汇编中切换第i个位置?
我尝试用C语言编写并查看大会,并不完全明白为什么会有一些东西存在。
C:
unsigned long toggle(unsigned long num, unsigned long bit)
{
num ^= 1 << bit;
return num;
}
int main()
{
printf("%ld\n", toggle(100, 60));
return 0;
}
从GDB切换功能组件:
<toggle>
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-0x8],rdi
mov QWORD PTR [rbp-0x10],rsi
mov rax, QWORD PTR [rbp-0x10]
mov edx, 0x1
mov ecx, eax
shl edx, cl
mov eax, edx
cdqe
xor QWORD PTR [rbp-0x8],rax
mov rax, QWORD PTR [rbp-0x8]
pop rbp
ret
有人可以引导我了解程序集上的内容,以便我能更好地理解这一点,并在x86-64中编写自己的切换功能吗?
答案 0 :(得分:4)
我想知道是否有一种很好的方法可以在x86-64汇编中切换第i个位置?
是,x86's BTC
(Bit Test and Complement) instruction does exactly that(以及将CF设置为该位的旧值),并在所有现代CPU上高效运行。
来源:Agner Fog's instruction tables and x86 optimization guide。另请参阅x86代码wiki中的其他效果链接。
toggle:
mov rax, rdi
btc rax, rsi
ret
(如果你在C中正确写了toggle
。
不要将btc
与内存操作数一起使用:位串指令具有疯狂的CISC语义,其中位索引不限于由寻址模式选择的双字内。 (所以btc m,r
是10 uops,Skylake每5c吞吐量一个)。但是对于寄存器操作数,移位计数与变量计数移位完全相同。
不幸的是,即使使用-march=haswell
或-mtune=intel
,gcc和clang也会错过这个窥孔优化。它甚至可以在AMD上使用,但它在英特尔上更有效率。
1ULL << bit
在btc
慢于xor
的AMD CPU上,值得在寄存器中生成掩码并使用xor
。或者甚至在Intel CPU上,在内存中切换一点是值得的。 (memory-destination xor
比memory-destination btc
好得多。
对于数组中的多个元素,请使用SSE2 pxor
。您可以使用以下命令生成蒙版:
pcmpeqd xmm0, xmm0 ; -1 all bits set
psrlq xmm0, 63 ; 1 just a single bit set
movd xmm1, esi
psllq xmm0, xmm1 ; 1<<bit
; then inside a loop, with data in xmm1
pxor xmm1, xmm0 ; flip bit in each qword element
并不完全明白为什么会有一些东西存在。
所有垃圾都在那里,因为你编译时没有优化,因为你使用了一个带符号的int
常量。
甚至不值得查看-O0
代码中的所有溢出/重新加载到内存。如果您想要的代码不吸引,请使用-O3 -march=native
进行编译。
另请参阅How to remove "noise" from GCC/clang assembly output?和 Matt Godbolt的CppCon2017演讲:“What Has My Compiler Done for Me Lately? Unbolting the Compiler's Lid” ,以便了解编译器生成的asm。
使用带签名的int
常量1 << bit
解释了为什么gcc执行32位移位然后cdqe
。 num ^= 1 << bit;
相当于
int mask = 1;
mask <<= bit; // still signed int
num ^= mask; // mask is sign-extended to 64-bit here.
在gcc -O3输出中,我们得到
mov edx, 1
sal edx, cl # 1<<bit (32-bit)
movsx rax, edx # sign-extend, like cdqe does for eax->rax
xor rax, rdi
如果我们写toggle
corectly:
uint64_t toggle64(uint64_t num, uint32_t bit) {
num ^= 1ULL << bit;
return num;
}
<强> (source+asm on the Godbolt compiler explorer) 强>
gcc和clang仍然错过使用btc
,但这并不可怕。有趣的是,MSVC确实发现了btc
窥视孔,但浪费了MOV指令:
toggle64 PROC
mov eax, edx
btc rcx, rax
mov rax, rcx
ret 0
使用uint64_t
位可避免额外的MOV。这是不必要的,因为具有寄存器目标的btc
会使用& 63
来屏蔽索引。高垃圾不是问题,但MSVC不知道这一点。
gcc和clang会像你期望的那样发出代码,但是gcc通过在1ULL <<bit
中生成rdx
并且必须复制到rax
来浪费MOV指令。
; clang output.
mov eax, 1
mov ecx, esi
shl rax, cl
xor rax, rdi
ret