转移一个巨大的数字 - 组装

Shifting a huge number - assembly

我有一个巨大的数字加载到堆栈上,我使用 eax 访问它。它 不能存储在寄存器中。我使用 eax 只是为了指向它的地址(数字是自然类型,这意味着前 4 个字节包含符号,接下来的 4 个字节包含长度,其他的是实际值)。

我必须移动它 edx 次。 我正在考虑从 LSB 开始逐位移位(最多 8 次/字节),然后将这些位复制到下一个字节中。为了做到这一点,我必须首先移动下一个字节,依此类推,直到 MSB 位置 + 1(最坏情况)或直到所有移位都完成并且没有进位标志。 P.S。我显然是在这种特殊情况下谈论 shl 但几乎同样适用于 shr.

有没有更简单的解决方案?

我宁愿 ROL 或 ROR 值,切掉翻转的位并将它们应用于下一个字节(在对其应用完全相同的过程之后)

经典的 8 位时代想法是使用由 DEC counter + JNZ 交错的 RCL(带进位的左移)——你可以暂停一下,最后欣赏一下,为什么 x86 DEC/INC 指令只影响零-标记,但不携带(谜底已解)。

因此代码将遵循以下几行:

    mov   edi,address_of_last_byte
    mov   edx,count_of_bytes
    mov   cl,1
    clc   ; clear CF
loop_1_bit_left:
    rcl   byte [edi],cl    ; CF -> LSB, MSB -> CF
    dec   edi    ; preserves CF! Goes from last byte to first one
    dec   edx    ; preserves CF! Decrement counter
    jnz   loop_1_bit_left  ; till whole buffer is shifted
    ; CF has last bit, will be thrown away unless you do something about it

现在还有很多不足之处...

如何保存buffer的MSB?我会首先计算移位后所需的缓冲区大小 (new_length = arg_length + (shift+7)/8))。并将输入复制到其中,然后不移动 arg_length 字节,而是移动 new_length 字节,这解决了 MSB 的 t运行cation 问题。

但是还有另一个问题,性能。不幸的是,现代 x86 CPU 上的 rcl 速度很慢,因此以这种方式移动 315 位是非常糟糕的主意。但你不必这样做。您可以首先将输入数字复制 39 个字节(向开始)到 new_length 缓冲区,然后通过上面的循环一个一个地进行剩余的 3 个位移位。

此外,如果您将足够填充输出缓冲区,则可以使用 dword/qword rcl 变体(32b/64b 代码)同时处理更多字节。 (实际上从你的描述中不清楚谁负责分配输出缓冲区,如果你的代码将 return 它以某种方式放在堆栈上(??我不确定根据 shift 动态增长的缓冲区在哪个 ABI 中是可能的amount),或者在堆上分配它,在顶部添加几个字节,这样你就可以在最后一个常规字节值之后修改几个字节,你可以使用 dword/qword 代替,加上超过 4/8B 对齐(! ) 地址)。


编辑:word/dword 引用变体 rcl/rcr 只有当数组中的整个大数字都遵循小端方式时才能正常工作x86 的,并且循环遵循正确的 ++/-- 方向(位 b0-7 在字节数组中的偏移量为 +0,例如位 b80-b87 的偏移量为 +10,右移将从 MSB 开始(+10) b87 朝向 LSB(+0) b0)。我最初的 byte [edi] 示例期望它采用大端方式,MSB 从偏移量 +0 开始,LSB 以 + 结束,因此可以按人类顺序查看位 b87 .. b0,即小端每个字节组在视觉上 "reversed" (b7 .. b0 b15 .. b8 ... ... ... b87 ...b80) ...至少我这么认为,现在我开始如此困惑。只需以一种方式编写代码,为简单的极端情况创建单元测试并验证结果 + 修复它以产生您期望的结果。 :D


在这种情况下,请确保不要将 edi 更新为 sub edi,4 (sub rdi,8),因为那样会破坏 CF 内容,因此请利用 lea edi[edi-4]通过寻址方式完成的简单计算方式。并调整计数器以获得正确的 /4 || /8 值。

为了获得最佳性能,一次移动 1-7 位可能仍然值得:对于剩余 1 位,您可以保留 rcl 版本,对于 2-7 位移动 masking/oring 值按单次目标数量移动,例如使用 32b 寄存器来处理 16b read/write 缓冲区并将移出的位保留在上半部分。或者,如果您要走那么远,也许可以分析带有 shl/and/or 的 1 位变体,无论它是否不比 rcl 变体快。由于 rcl 不被编译器使用,特定的 CPU 可能更喜欢多个 shl/and/or 指令而不是单个 rcl.


有趣的事实:我完全独自编写的第一个 Z80 汇编代码就是这样做的,将一个巨大的内存区域向左(和向右)移动 1 位。由于那个巨大的内存区域实际上是 ZX Spectrum 计算机的视频内存,它实际上是将图像 left/right 移动 1 个像素(ZX 每个像素使用 1 位)。

而且我没有意识到可以使用 CF 从一个轮换到另一个轮换,所以我通过单独屏蔽该位,将其复制到其他寄存器,然后从那里将其恢复到新字节等来做到这一点。

所以我写了它,运行它(因为 bug 重置了 ZX),修复了 bug,运行它,然后观察图像是如何移动的……大约 10 次比我预期的 "almighty fast Assembly code" 慢(大约每秒 3 帧)。然后我的一个朋友确实向我展示了如何旋转它,这使得代码 运行 接近 20 FPS(这仍然让我意识到即使 "fast assembly" 也不是无限的,我必须计算为了让 ZX 的屏幕上看起来像样,我的代码很多)。