将一个寄存器的位复制到另一个寄存器(x86-64 asm)



作为在运行时生成x86-64机器代码的项目的一部分,我经常需要将一个位从一个寄存器复制到另一个位位置的另一个寄存器。

我想出了这样的代码(将源寄存器的第23位复制到目的地的第3位的示例):

bt eax, 23           ; test bit 23 of eax (source)
setc ebx             ; copy result to ebx (ebx is guaranteed to be zero before)
shl ebx, 3           ; shift up to become our target bit 3
and ecx, 0xFFFFFFF7  ; remove bit 3 from ecx (target)
or ecx, ebx          ; set new bit value

假设我只需要五条指令就可以将一个寄存器的一位复制到另一寄存器的另一位,我想x86上是否有使用较少指令的东西?

我读了一些BMI的说明,但不幸的是,它们没有提供使用即时提取的比特。

指令数不是一个好的性能指标以字节为单位的代码大小(x86机器指令是可变长度的),或者现有主流CPU执行它们的方式:前端uops的数量(解码后)和/或延迟/吞吐量是相关的优化目标,重要的目标取决于周围的代码。(在预测现代超标量处理器上的操作延迟时需要考虑哪些因素?我如何手动计算它们?)

rcr/rcl非常慢,尤其是计数!=1
四条单uop指令的速度要快得多,包括BMI2rorx,用于复制要插入的位并将其旋转到tmp寄存器中的正确位置,然后是and,用于隔离它。或者,如果不需要保留输入,则可以使用普通移位/。这比我想的任何通过CF.的方法都更有效率

这比xor-零/bt/setc/shl更有效。它还避免了部分寄存器的错误依赖或暂停:setc ebx不存在,只有setc bl(或setc bh)。这也意味着,如果你可以销毁你的输入寄存器,而不是使用临时寄存器,你就不需要像setc bl/movzx ebx, bl这样效率低下的东西,这会使零扩展成为延迟的关键路径,并击败mov消除。

我临时切换到EDX,因为它在正常的呼叫约定中会被呼叫阻塞。

; input in EAX, merge-target in ECX (read/write)
; no pre-conditions necessary
;  unlike the original which doesn't count the cost of zeroing EDX
rorx  edx, eax, 23-3       ; shift bit 23 to bit 3.  Use SHR if you don't need the EAX value later
and   edx, 1<<3            ; and isolate
and   ecx, 0xFFFFFFF7      ; clear it in the destination
or    ecx, edx             ; and merge
; total size: 14 bytes of machine code for imm8 masks, 20 for imm32 masks
; 4 uops.

如果您愿意,or可以是addlea,因为我们知道不存在重叠的1位。lea作为复制和合并非常有用,以防您希望结果在不同的寄存器中。但如果你想要的话,你只需要将or而不是ECX放入temp-reg中,你可以选择任何你想要的temp-reg,包括EAX(在这种情况下,你可以将rorx优化为shr。)如果你想在FLAGS上从它分支,add会很有用,因为它可以与Sandybridge家族上的某些形式的jcc宏融合。xor也可以,但没有任何优势,对人类读者来说也不是惯用语。

如果您不介意破坏输入,lea可以用于只允许shr而不是更长的rorx。2寄存器LEA比addor长1个字节,但在大多数CPU上,当没有移位计数(AMD)和恒定位移时,速度很快。(在Ice Lake之前,它不能在Intel上的那么多端口上运行,所以如果其他端口都相等,请使用addor,即当你无法用lea保存任何insns或延迟时。)clang在-O3中很好地利用了它(只是tune=generic,没有-march):https://godbolt.org/z/Kbbh6zs4W;它还生成与CCD_ 34相同的SHR/AND/AND/LEA。(我想它不会考虑使用rorx来保留输入,即使在以后编译确实需要它的代码时也是如此。)

这些都是Intel和AMD上的单个uop指令,并且您的目标位位置足够低,两个and掩码都可以放入一个有符号的imm8中,因此and指令各为3字节。(而不是and r/m32, imm32的6)。不过,rorx是6个字节,带有VEX前缀和imm8。总大小为14个字节,如果目标位在低位7之外,则为20个字节。(如果使用字节操作数大小(如and dl, 0x80/…/or cl, dl),则为低8,这会导致P6系列的部分寄存器问题,但在其他地方也可以。)

(问题中使用的指令也是单个uop,包括bt。在AMD CPU上,bts等是2个uop,但bt只有1个。)

使用更高的目标位数,可以使用btr ecx, 30(4字节,在英特尔上仍然是1 uop)而不是and ecx, ~(1<<30)(6字节,或EAX中的5字节)来节省大小。但这在AMD上需要额外的uop。

当然,如果您关心代码大小,您应该使用mov edx, eax/ror edx, 23-3(总共5个字节),而不是使用rorx(6个字节)。因此,这总共是17个字节,具有较高的目标位pos。或者15,如果我们能摧毁EAX。

如果位位置是运行时变量,则效率会降低,需要变量计数移位。(还有一些减法或生成移位计数的东西。)不同的策略可能会更好。


在寄存器之间交换位的另一种方法是使用掩码XOR,但在我们不想交换的情况下,这并不更有效,只需走一条路。我们可以使用反转掩码作为立即数。(或者,如果在寄存器中,则使用BMI1andn。)

  • 根据掩码合并位序列a和b
  • 如何使用逐位操作根据另外两个字节分配字节的特定位?(根据掩码进行位混合)
  • 在两个字节之间的给定点交换位

主要问题是x86缺少位字段insert。使用shift/和进行提取很容易,尽管这仍然是2条指令,除非您有使用XOP编码的bextr的直接形式的AMD TBM。(仅限推土机系列。)或者,如果寄存器中已有常量,则使用pext。如果有一个bt的逆函数将CF沉积在指定的位置,那将是很好的,但不幸的是没有。

如果有一个位字段insert指令,无论是来自CF还是来自另一个寄存器的低位,您都不需要屏蔽。


在没有rcr/rcl的情况下避免温度调节-仅稍慢,仍为4 uops

@rcgldr展示了使用rcr/rcl的一个有趣技巧,它对代码大小很好,但不幸的是,在rclrcr r32, imm是许多uop的现代CPU上速度很慢,例如Zen3上的7个uop具有3c吞吐量,Sandybridge家族(包括Alder Lake)上的8个uops具有6c吞吐量。(https://uops.info//https://agner.org/optimize/)

这是10个字节的机器代码,不需要临时寄存器。我们只需要12字节的机器代码就可以复制该功能,但假设有足够现代的x86,仍然只有4条单一的uop指令。上述版本在英特尔Haswell及更早版本上会更快。

这具有比ECX更长的关键路径延迟->结果(3个周期),但对于EAX->结果(3个循环),假设单个uopadc并旋转。此外,更多的uop竞争移位单元,因此无法在如此广泛的后端端口上运行。这是否重要取决于周围的代码。

即使当目的比特位置>并且对于64位模式避免了需要任何8字节掩码。

如果你没有一个备用寄存器,你可以对它进行缓冲(包括输入),这很可能是值得的。或者只是为了代码大小,如果这不是你代码中非常热门的部分。

;; 12 bytes total.  More latency through ECX, and some uops have fewer ports to choose from
ror   ecx, 3+1         ; 1 uop on Intel HSW and later, and AMD
; the bit to be replaced is now at the top of ECX, and in CF
bt    eax, 23          ; 1 uop
adc   ecx, ecx         ; 1 uop on Broadwell and later, and AMD.
; Shift in the new bit, shifting out the old bit (into CF in case you care)
rol   ecx, 3           ; 1 uop on HSW and later, and AMD
; restore ECX's bits to the right positions, overwriting CF

初始向右旋转可以是rcrror;我们不在乎我们要替换的比特是暂时转移到顶部比特,还是只转移到CF。ror要快得多。

我们基本上用rcl ecx, 1rol ecx, 3来模拟rcl ecx, 3+1。我认为它在FLAGS输出上有所不同,但在ECX结果和从FLAGS读取的方式上相匹配。

然后用等效但更快的adc same,same代替rcl r32, 1;它们仅在FLAGS输出方面不同。adc没有任何奇怪的部分标志写入(使SPAZO的大部分不受影响),这使得Intel上的轮换更加昂贵。在Broadwell之前,adc在Intel上是2个uop,但在AMD上已经是1个uop了很长时间。

这通过使用bt的FLAGS进行,因此它可以轻松地支持运行时可变源位位置。对于可变的目标位位置,您必须计算移位计数,而ror reg, cl的速度较慢(在Intel上为3 uops)。不幸的是,没有变量计数rorx,只有shlx/shrx

替代方案:

rcr     ecx,3+1
bt      eax,23
rcl     ecx,3+1

相关内容

  • 没有找到相关文章