在 68k 数据寄存器中复制符号值的最快方法。 (整数的无分支条件否定)

Fastest way to copy sign value among 68k data registers. (Branchless conditional negation of an integer)

我正在寻找一种优化的方法来使用另一个数据寄存器中的值的符号来更改数据寄存器中包含的值的符号。

源寄存器将包含 1 或 -1,而目标寄存器始终为正,因此只需要乘法即可。这是我将要执行的操作的一些伪代码:

MOVE.B #-1,D0
MOVE.W #173,D1
MULS.W D0,D1

在这个简单的数学运算之后,D1 将携带 D0 的符号并变为 -173 或 173。这是期望的结果,但 MULS 最多需要 70 个周期,我希望通过找到某种技巧来节省一些仅“复制”标志。

最后一句话:技巧应该是无分支的,因为分支是我试图通过首先复制符号来防止的。

提前感谢您提供任何信息或建议。

您可以使用 bit-hack 在 x-x 之间无分支地 select。

但是你可以做得更好:2的补非可以表示为-x == ~x + 1,非和增量都可以用-1的XOR和SUB表示。但是使用 0 的相同操作是空操作,保持值不变。

(这个技巧通常用于2的补码绝对值,其中0或-1是通过算术右移得到的,x >> 31,将符号位复制到寄存器的所有位。即应用您的条件符号翻转操作与给我们符号位的数字相同。https://graphics.stanford.edu/~seander/bithacks.html#IntegerAbs)

当然,您需要一个 WORD 大小的 -1,而不仅仅是 BYTE,因为您需要覆盖所有要取反(或不取反)的值位;正如 Erik 指出的那样,这意味着 move.w #-1, d0ext.w 如果您不能将字节值提升为原点的单词。

;; d0 = word  1 or -1
   asr.w     #1, d0
;; d0 = word  0 or -1

   eor.w     d0, d1           ;  ~d1 
   sub.w     d0, d1           ;  ~d1 - 1  (or unchanged for d0=0)

; d1 = d1 or -d1  according to d0

如果你想根据其他数字的符号取反,直接生成一个0/-1而不是创建一个1 / -1.
(即一个整数 copysign,如果 ISO C 有一个 signum 函数,有点像乘以 signum(y),但没有在 y=0 上归零。)

通常您会使用 x >> 15 进行算术右移,但 M68K 立即移位只允许从 1..8 开始计数。 (在没有桶形移位器的情况下,CPUs 上的大量计数很慢)。所以你实际上想要通过符号扩展到 32 位来获得符号位的 16 个副本,然后交换一半以将其放置到位:

;; manufacture a 0 / -1 word according to d0.w < 0
   ext.l   d0       ; high 16 bits = 0 / -1
   swap    d0       ; those are now the low bits

; optionally: ext.l  d0   ; to make a 32-bit 0 / -1

对于 32 位源,您可以 tst.l d0,d0 / smi d0(设置为 MInus)以生成字节大小的 0 / -1,然后对其进行符号扩展。 (虽然只有 68020 可以在一个 extb.l 指令中完成;68000 需要 ext.w + ext.l


如果您有 MC68020 或更高版本 CPU,您可以仅使用符号位的符号扩展位域提取。 bfexts d0 {15:1}, d0。这是一条 4 字节指令,代码大小与 ext.l + swap 相同,但它扩展到 32 位并且可以在 32 位源寄存器上工作而无需额外的指令。即使对于 16 位整数,它也是一条指令而不是 2 条指令,所以它可能很好,特别是如果它是从缓存中执行的?或者不是,因为它似乎更慢。

其他选择:

  • moveq #15 通过 15 或 31 进入 asr 的寄存器。即使在 m68k 上也比 ext/swap.
  • 向左循环1,and-立即用1将符号位隔离为0/1,neg。对于 32 位输入很容易工作,可能很好,尤其是在 68000 上,31 位的移位可能很慢。但是 rol 在 68020 上速度较慢。

https://www.nxp.com/docs/en/data-sheet/MC68020UM.pdf 包括 68020 的指令时序,作为“最佳情况”(与前一条指令的执行重叠,缓存中很热)、“缓存情况”(仅缓存,无重叠)和“最坏的情况”(两者都没有)

  • bfexts Dn:5 个周期(最佳)/8 个(缓存)/8 个(最差)
  • EXT Dn : 1(最佳)/ 4(缓存)/ 4(最差)
  • SWAP Rx,Ry : 1(最佳)/ 4(缓存)/ 4(最差)

因此,当 ext/swap 可以完成工作时,bfexts 可能不是 16 位输入的好选择。

  • ASR Dn:3(最佳)/6(缓存)/6(最差)。有趣的是,立即数逻辑移位比这更快,但算术移位是一样的。 (68020 显然有一个桶形移位器;性能不取决于计数。68000 可能没有,IDK。)
  • MOVEQ #<data>, Dn:0(最佳)/2(缓存)/3(最差)
  • ROL Dn:5(最佳)/8(缓存)/8(最差)
  • ANDI #<data>,Dn:0(最佳)/2(缓存)/3(最差)
  • EOR Dn,Dn:0(最佳)/2(缓存)/3(最差)
  • SUB EA,Dn:0(最佳)/2(缓存)/3(最差)。获取 Dn 源的 EA 时间为 0 个周期。

重叠(创造最好的情况)取决于前一条指令有多慢,我认为,尤其是内存访问而不是核心周期;我链接的手册有一些细节,但它们也很重要,你不能只把最好或最坏的情况加起来,除了 lower/upper 界限;您需要进行基准测试才能找到典型的 运行 次。 IDK 数据依赖性对此有何影响。

这些是 68020 个时间,因为我们可以选择 bfextsext/swap。在 68000 上,ext/swap 几乎肯定比任何替代方案都要好。