切换特定位

时间:2017-11-10 17:23:15

标签: c assembly x86 bit-manipulation

所以我看到了类似的问题 toggle a bit at ith positonHow 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中编写自己的切换功能吗?

1 个答案:

答案 0 :(得分:4)

  

我想知道是否有一种很好的方法可以在x86-64汇编中切换第i个位置?

是,x86's BTC (Bit Test and Complement) instruction does exactly that(以及将CF设置为该位的旧值),并在所有现代CPU上高效运行。

  • Intel SnB系列:1 uop,1c延迟,每时钟吞吐量2。 (Nehalem和更早:每个时钟1个)
  • Silvermont / KNL:1 uop,1c延迟,每时钟吞吐量1。
  • AMD Ryzen:2次uop,2c延迟,每时钟吞吐量2次
  • AMD Bulldozer-family / Jaguar:2 uop,2c延迟,每时钟吞吐量1
  • AMD K8 / K10:2次微秒,2c延迟,每时钟吞吐量1次

来源:Agner Fog's instruction tables and x86 optimization guide。另请参阅代码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位移位然后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<<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