开关特定位

8 浏览
0 Comments

开关特定位

我看到了类似于在第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切换功能了吗?

admin 更改状态以发布 2023年5月23日
0
0 Comments

我想知道在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

btcxor慢的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位的位移,然后cdqenum ^= 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;
}

(在Godbolt编译器资源管理器上的源代码汇编)

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

0