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

4
作为生成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指令,但不幸的是它们不能使用立即数进行位提取。


问题中的位位置是固定的吗? - fuz
是的,它们是即时数。 - Lyve
“setcc”指令只允许设置8位寄存器。不过,如果你在示例中将其更正为“bl”,它仍然可以工作,因为你已经评论说在此之前“ebx”为零。 - ecm
2个回答

4

指令数量不是一个好的性能指标。无论是以字节为单位的代码大小(x86机器指令长度可变),还是针对现有主流CPU执行它们的方式:前端操作(解码后)的数量和/或延迟/吞吐量是相关的优化目标,具体取决于周围代码的重要性。对于这么小的东西,其中一个最重要的目标是什么。(如何预测现代超标量处理器上操作的延迟,并如何手动计算它们?

rcr / rcl非常缓慢,特别是count != 1的情况。
四个单操作码指令更快,包括使用BMI2 rorx将您想要插入的位复制并旋转到tmp寄存器中的正确位置,然后使用and来隔离它。如果您不需要保留输入,则使用普通的移位/和。那比我想到的任何通过CF弹跳的方法都更有效。

这比使用xor-zero/bt/setc/shl更有效率。它还避免了局部寄存器假依赖或停顿: setc ebx不存在,只有setc bl(或setc bh)。这也意味着,如果你可以破坏你的输入寄存器而不是使用临时寄存器,你就不需要像setc bl/movzx ebx, bl这样效率低下的指令,因为这会让零扩展成为延迟的关键路径,并且破坏mov-elimination。
我将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寄存器,您可以选择任何临时寄存器,包括EAX寄存器(在这种情况下,您可以将rorx优化为shr)。add在某些情况下与Sandybridge系列上的一些形式的jcc宏融合,因此非常有用,例如当您需要根据FLAGS进行分支时。xor也可以使用,但没有任何优点,并且对于人类读者来说不是习惯用法。

lea可以用于允许仅使用shr而不是更长的rorx,如果您不介意破坏输入。2寄存器LEA比addor多1个字节,但是当没有移位计数(AMD)和无常量位移时,在大多数CPU上速度很快。(在Ice Lake之前,它不能在Intel上运行太多端口,因此如果所有其他条件相等,则使用addor,即当您无法使用lea保存任何指令或延迟或其他东西时。)clang在使用-O3(只需tune=generic,没有-march)时对其进行了很好的利用:https://godbolt.org/z/Kbbh6zs4W;它还使用-march = haswell 生成相同的SHR / AND / AND / LEA。(我猜测即使编译需要稍后使用它的代码,它也不会考虑使用rorx来保留该输入。)

这些指令都是Intel和AMD上的单操作码指令,并且你的目标位位置很低,所以两个AND掩码都可以适合一个符号扩展的imm8,因此AND指令每个为3个字节。(而不是6个字节的and r/m32, imm32)。rorx是6个字节,带有VEX前缀和imm8。总大小为14个字节,如果目标位超出了低7位,则为20个字节。 (或者低8位,如果您使用像and dl, 0x80 / ... / or cl, dl这样的字节操作数大小,会在P6系列上引起部分寄存器问题,但在其他地方没有问题.)
(问题中使用的指令也是单操作码,包括bt。在AMD CPU上,bts等指令是2个操作码,但bt只有1个操作码。)
使用更高的目标位数,您可以使用btr ecx, 30(4个字节,在英特尔上仍然是1个uop)而不是and ecx,~(1<<30)(6个字节或5个字节进入EAX)来节省空间。但这会在AMD上增加一个额外的uop。
当然,如果您关心代码大小,您可以使用mov edx,eax / ror edx,23-3(总共5个字节)而不是使用rorx(6个字节)。因此,具有高目标位位置的总共为17个字节。或者如果我们可以破坏EAX,则为15个字节。
如果位位置是运行时变量,则效率会降低,需要进行可变计数移位。(并进行一些减法或其他操作以生成移位计数。)在那种情况下,可能会有更好的不同策略。
另一种在寄存器之间交换位的方法是使用掩码异或,但在这里我们不想交换,只是单向移动。我们可以使用反转的掩码作为立即数。(或如果在寄存器中,则使用BMI1 andn。)
主要问题是x86缺少一个位域插入指令。使用移位/与提取很容易,尽管这仍然需要2个指令,除非您拥有AMD TBM的立即形式的bextr,使用XOP编码。(仅适用于Bulldozer系列) 或者如果您已经在寄存器中有一个常量,则使用pext。如果存在bt的反向操作可以将CF放置在指定位置,那就太好了,但不幸的是没有这样的指令。
如果有一个位域插入指令,无论是来自CF还是另一个寄存器的低位,您都不需要进行掩码。
避免使用rcr/rcl而不需要临时寄存器-稍微慢一些,但仍然是4个uops。 @rcgldr展示了一个有趣的技巧,使用rcr/rcl可以减小代码大小,但在现代CPU上速度较慢,例如Zen3上rclrcr r32, imm需要7个uops,吞吐量为3c,而Sandybridge系列(包括Alder Lake)需要8个uops,吞吐量为6c。(https://uops.info/ / https://agner.org/optimize/
这是10字节的机器码,不需要临时寄存器。我们可以用只有12字节的机器码来复制功能,但仍然只有4个单uop指令,假设x86足够现代化。上面的版本在Intel Haswell及更早版本上速度更快。
这个操作从ECX到结果的关键路径延迟更长(3个周期),但是对于EAX到结果(3个周期)来说相同,假设单个uop adc和旋转。此外,更多的uops竞争移位单元,因此不能在后端端口的选择上运行得那么广泛。这是否重要取决于周围的代码。
即使目标位位置> 8,在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, 1 然后 rol ecx, 3 来模拟 rcl ecx, 3+1。我认为它们在 FLAGS 输出上有所不同,但 ECX 结果匹配并且从 FLAGS 中读取的方式相同。

然后用等效但更快的 adc same,same 替换 rcl r32, 1;它们只在 FLAGS 输出上有所不同。 adc 在 Intel 上没有任何奇怪的部分 FLAGS 写入(大多数 SPAZO 不受影响),这使得旋转在 Intel 上更加昂贵。在 Broadwell 之前,adc 在 Intel 上需要 2 个微操作,但在 AMD 上已经是 1 个微操作很长时间了。

这个使用bt来遍历FLAGS,因此可以轻松支持运行时可变的源位位置。对于目标位位置的可变性,您需要计算移位次数,而ror reg, cl则速度较慢(在Intel上需要3个uop)。不幸的是,没有可变计数的rorx,只有shlx / shrx

@rcgldr:没错,我的代码也是一样的。再次感谢您注意到adc ecx, 0而不是adc ecx, ecx毫无意义。 - Peter Cordes
@rcgldr:在您的IvB上,我的第一个版本,用mov/shr(或者如果您不需要原始版本,只用SHR)替换了rorx,仍然是4个uops,但基于旋转的版本则更多uops。在旧款Intel上表现良好是一个显著的优势,这也是我在答案中首先保留它的原因。在Skylake上,包括您的Comet Lake,在某些用例中第二个版本可能具有相同的速度,除非从ECX到输出的关键路径延迟很重要。(2c vs. 3c) - Peter Cordes
1
@njuffa:就像我说的那样,它只有在能够复制和合并时才有用,但这是不必要的。使用索引寻址模式(2个寄存器)需要额外的一个字节,并且在某些CPU上不能运行在更多的后端ALU端口上。在旧的顺序Atom CPU上,它在管道中运行得更早,这对于转发到它与从它转发的延迟有影响。我不想在那段话中加入更多的lea细节;我希望大多数读者能够理解(我暗示)它是没有用的,但我认为你说这还不够明显。 - Peter Cordes
@PeterCordes 自从Clang和最新的Intel编译器在这种情况下似乎更喜欢“lea”,我想知道它们之间的权衡是否显而易见。 对我来说并不明显。 #define SRC_BIT(23)#define DST_BIT(3)uint32_t bit23_to_bit3(uint32_t a,uint32_t b){return(a &(1 <<SRC_BIT))>>(SRC_BIT-DST_BIT)|(b &〜(1 <<DST_BIT));} - njuffa
@njuffa:啊,但是clang在那里没有使用rorx;我只是在谈论你使用的情况。但实际上,在那里使用LEA可以节省代码大小,并且不应该影响延迟,因为该寻址模式足够简单,不会成为任何主流CPU上的慢速LEA,如果我没记错的话。更新了我的答案,提到了使用lea作为shr的用例。 - Peter Cordes
显示剩余3条评论

3

替代方案:

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

1
这意味着指令数量减少了,但不幸的是速度并没有提升。在现代CPU上,除了1以外的计数的RCL/RCR需要多个uops。例如,在Skylake上,rcr/l r32, imm8cl需要8个uops,并且吞吐量为6个周期。(https://uops.info/甚至使用了一个打破FLAGS依赖性的指令和多个dep链进行测试,但仍然发现吞吐量为6c)。在Zen3上情况会稍微好一些,但每个旋转通过Carry分别需要7个uops和3个周期的吞吐量。普通旋转和rcr-by-1更便宜,因此您可以通过旋转3次,然后rcr 1/bt/rcl 1,最后再正常旋转来提高性能。 - Peter Cordes
@PeterCordes - 关于卡莫湖呢? - rcgldr
Comet Lake的内核微架构与Skylake相同,除了一些Spectre / Meltdown缓解措施。我不知道为什么uops.info在其表格中甚至要单独列出Kaby Lake和Coffee Lake的条目;也许只是为了实验性地证明它们与SKL没有任何区别?或者可能有一些我不知道的不同指令。Agner Fog还向他的指令表中添加了一个Coffee Lake部分,但我还是不知道为什么;他的微架构指南将其与SKL分组,并且没有提到任何差异。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接