开关特定位
开关特定位
我看到了类似于在第i个位置上切换位和如何设置、清除和切换单个位?这样的问题,但我想知道在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中的切换功能汇编:
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切换功能了吗?
我想知道在x86-64汇编中有没有好的方法来切换第ith位?
是的,x86的BTC
(位测试和补码)指令可以做到这一点(以及将CF设置为比特的旧值),并且在所有现代CPU上运行效率高。
- 英特尔Snb系列:1个uop,1c延迟,每个时钟周期2个(Nehalem及更早版本:每个时钟周期1个)
- Silvermont/KNL:1个uop,1c延迟,每个时钟周期1个
- AMD Ryzen:2个uops,2c延迟,每个时钟周期2个
- AMD Bulldozer系列/Jaguar:2个uops,2c延迟,每个时钟周期1个
- AMD K8/K10:2个uops,2c延迟,每个时钟周期1个
来源:Agner Fog的指令表和x86优化指南。请参阅x86标签wiki中的其他性能链接。
toggle: mov rax, rdi btc rax, rsi ret
(如果您在C中正确编写了toggle
的话)。
请勿将btc
与内存操作数一起使用:位字符串指令具有疯狂的CISC语义,其中位索引不仅限于由寻址模式选定的双字中。 (因此,在Skylake上,btc m,r
是10个uop,每个时钟周期5c的吞吐量)。但是,使用寄存器操作数时,移位计数与变量计数移位的掩码完全相同。
不幸的是,即使使用-march=haswell
或-mtune=intel
,gcc和clang也会忽略此peephole优化。即使对于AMD来说也值得使用,但对于Intel来说效率更高。
多个输入重复使用相同的1ULL << bit
在btc
比xor
慢的AMD CPU上,将掩码生成到寄存器中并使用xor
是值得的。即使在Intel CPU上,将位于内存中的单个位切换为值得的。 (内存目标xor
比内存目标btc
更好)。
对于数组中的多个元素,使用SSE2pxor
。您可以使用以下方式生成掩码:
pcmpeqd xmm0, xmm0 ; -1 all bits set psrlq xmm0, 63 ; 1 just a single bit set movd xmm1, esi psllq xmm0, xmm1 ; 1<
不太明白为什么会有这些东西在那里。
所有的这些都是由于您编译时没有启用优化,并且使用了有符号的
int
常量。
如果要编写不差的代码,请使用
-O3 -march=native
进行编译,不值得查看所有从-O0
代码中的溢出/重新加载到内存的东西。请参阅如何从GCC/clang汇编输出中去除“噪音”?和Matt Godbolt的CppCon2017演讲:“编译器最近替我做了什么?开启编译器的盖子”,以获取查看编译器生成的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<rax xor rax, rdi
如果我们正确编写
toggle
:uint64_t toggle64(uint64_t num, uint32_t bit) { num ^= 1ULL << bit; return num; }
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通过在
rdx
生成1ULL <
并且必须复制到 rax
来浪费MOV指令。; clang output. mov eax, 1 mov ecx, esi shl rax, cl xor rax, rdi ret