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

KON*_*NEY 2 optimization assembly signed bit-manipulation 68000

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

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

MOVE.B #-1,D0
MOVE.W #173,D1
MULS.W D0,D1
Run Code Online (Sandbox Code Playgroud)

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

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

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

Pet*_*des 6

您可以使用位黑客在x和之间进行无分支选择。-x

但你可以做得更好:2 的补数否定可以表示为-x == ~x + 1,NOT 和增量都可以用 的 XOR 和 SUB 表示-1。但与 a 相同的操作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
Run Code Online (Sandbox Code Playgroud)

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

通常您会使用x >> 15算术右移,但 M68K 立即移位仅允许从 1..8 开始计数。(对于没有桶形移位器的 CPU 来说,计算大量数据时速度很慢)。因此,您实际上希望通过符号扩展到 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
Run Code Online (Sandbox Code Playgroud)

对于 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到寄存器中asr乘以 15 或 31。即使在 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(最差)

因此,bfexts对于 16 位输入来说,当 ext/swap 可以完成这项工作时,这可能不是一个好的选择。

  • 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 个周期。

我认为重叠(创建最好的情况)取决于前一条指令的速度,尤其是内存访问而不是核心周期;我链接的手册有一些细节,但它们也提出了一个重要观点,即除了下限/上限之外,您不能仅将最好或最坏的情况相加;您需要进行基准测试才能找到典型的运行时间。我不知道数据依赖性如何影响这一点。

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

  • @chtz:68020 可以使用 `bfexts d0 {15:1}, d0` 或 `bfexts d0 {31:1}, d0` 将符号位作为 1 位字段符号扩展到完整寄存器中。否则,是的,tst/smi / ext/ext 可能对 32 位源有用,尽管 swap/ext.t/swap/ext.l 也适用于 32 位输入。我想对于代码大小,moveq-immediate 和 asr 对于 68000 上的 32 位值来说是最好的。无论如何,感谢 m68k 的详细信息,更新了我的答案的这一部分以使其实际工作。 (2认同)
  • @KONEY:啊,如果 -1 / +1 值实际上在程序的其他部分有用,那么这是一个有效的权衡。不过,-1 / 0 可以用 NOT 轻松翻转;1 的补码逆,但您不能将其添加到 inc 或 dec 的其他内容中,因此仍然有一个缺点。 (2认同)