检查 ax 是否可以被 16 整除

问题描述

我们如何检查 ax 是否可以被 16 整除?
我知道我们可以通过这个命令:

AND 0x000f

有更快的命令吗? (我认为 idiv 较慢)

解决方法

是的,and 是最快的指令之一,吞吐量和延迟与 add 等指令相同,idiv1。 (https://uops.info/https://agner.org/optimize/Why does C++ code for testing the Collatz conjecture run faster than hand-written assembly?(因为编译器使用了移位而不是 DIV。)


如果您要进行分支,test al,0x0f / jnz not_multiple_of_16 在某些 CPU(例如 AMD,包括 Zen,或 Intel Nehalem 及更早版本)上的test al,imm8 / and eax,0xf 甚至更快可以将 TEST 与 JCC 进行宏熔断,但不能与 AND/JCC 进行宏熔断。 TEST 类似于 AND 但只设置 FLAGS 而不写入目的地。 (因此以后对 AX / EAX 的读取不会在其路径中产生 AND 的额外延迟,并且能够读取原始值。)

此外,test eax,0xf 只有 2 个字节,与 3 字节 and eax,0xf 或 5 字节 and al,0xf2 相比,节省了代码大小。 (我假设是 32 位或 64 位模式;直到写完这篇我才注意到标题中的 AX,这暗示您可能正在针对 16 位模式进行优化。总体上没有显着差异。)

如果您想将寄存器值本身修改为实际以 16 为模,则可以使用 and al,0xf。 (不是 test al,0xf,您需要将高字节归零)。否则保留 EAX 不变,只写 FLAGS。

Sandybridge-family can macro-fuse AND/JCC,但 setnz cl 会写入 AL,如果您想稍后在 P6 系列 CPU(Nehalem 及更早版本)上读取 EAX,则会引入部分寄存器停顿。 (Why doesn't GCC use partial registers?)。在 SnB 上,RMW 操作不会将 AL 与 EAX 分开重命名,因此以后不需要在那里进行合并,并且它不是 false 依赖项,因为您明确想要测试

如果你想在另一个寄存器中得到 0 / 1 结果,那么 xor ecx,ecx test al,0xf setnz cl ; ECX = bool(x % 16U) / setnz 是好的。

cmovnz

(如果您使用 testand,则 andtest 没有任何优势,除了不修改 EAX。宏融合仅在测试之间和条件分支,而不是 setnz。所以如果你还想修改寄存器,就像创建一个布尔值一样,你可以在这里使用 idiv 而不是 div。)


脚注 1:idiv 慢得多, 需要额外的指令来设置 EDX,并将除数放在另一个寄存器中,并且不会根据结果设置 FLAGS以一种有用的方式。事实上,int 0x80syscall 是大多数 CPU 上最慢的整数数学指令,只有其他大量微编码的指令,如 rep movsbx / 10,或者例如大 { {1}} 变慢了。

最近的 CPU,如 Broadwell 和后来的 CPU 具有相当高性能的硬件分区(https://uops.info/https://agner.org/optimize/),而且显然 Ice Lake 对其进行了更多改进(尤其是对于 64 位操作数大小),但是编译器会努力避免分裂。例如编译器将使用多个其他指令来实现 int x,即使对于带符号的 unsigned mod16(unsigned x) { return x % 16; } int mod16_signed(int x){ return x % 16; // negative for negative x,can't just use AND } ,这不仅需要乘法逆除法,而且还需要一些符号位处理以向 0 舍入,即使对于负数也是如此。 Why does GCC use multiplication by a strange number in implementing integer division?

当然对于 2 的除数,编译器知道他们可以使用 AND。

-O3 -m32 -mregparm=3

asm on the Godbolt compiler explorer。用 clang 和 GCC # GCC and clang of course do this mod16: and eax,15 ret 编译(所以第一个 arg 到达 EAX,并在 EAX 中返回。)

# clang12.0 -O3 -m32 -mregparm=3
mod16_signed:
        lea     ecx,[eax + 15]
        test    eax,eax                 # set FLAGS from x
        cmovns  ecx,eax                 # ecx = !(x<0) ? x : x+15
        and     ecx,-16                 # round ecx down to a multiple of 16
        sub     eax,ecx                 # return  x - round_down(ecx)
        ret

但签名更难:

cmov

Clang 具有一些指令级并行性(LEA 和 TEST 可以并行运行),并且可能最适合具有单 uop cmov 的现代 CPU(AMD 和 Intel Broadwell 及更高版本)。 GCC 的所有 5 条指令都依赖于前一条指令,因此有 5 个周期延迟,而 clang 则为 4 个。或者在带有 2-uop # gcc10.3 -O3 -m32 -mregparm=3 mod16_signed: cdq shr edx,28 # edx = (x>=0) ? 0 : 0xf add eax,edx # eax = (x>=0) ? x : x+15 and eax,15 sub eax,edx # if(x<0) eax++ ret 的 CPU 上也有 5 个。

GCC 使用了稍微不同的策略,使用 cdq 将符号位广播到 EDX,然后右移 28 以在 EDX 中获得 0 或 0xf。

test r/m32,sign_extended_imm8

脚注 2TEST 没有 test ax,-8 形式,这与作为原始 8086 一部分的所有其他立即指令不同。它在原始版本中很少有用8086,仅适用于设置了 MSB 的情况(因此上半部分是全 1),因此您想测试是否设置了任何位除了一些在低 8 中。例如ax >> 3 就像检查 x & 0 = 0, 是否为非零。

test al,1 所以 test ax,1 总是与 test ah,imm8 相同;它不写目的地,所以只有 FLAGS 结果很重要。如果您愿意,您可以执行 test ax,imm16,或者 {{1}} 在 16 位模式下只有 1 个额外字节,因此在设计 8086 时不会丢失很多节省。这是 32 位操作数大小的 3 字节差异,但现代 CPU 通常不会在代码获取上出现瓶颈。

(较小的通常可以更好地减少整体 L1i 缓存未命中,并且通常可以更好地打包到 uop 缓存中,并且整体较小的二进制文件从磁盘加载速度更快,iTLB 未命中更少,因此编译器应该支持较小的代码 相等。通常不值得使用较慢的指令来节省代码大小,但仍然值得稍微展开热循环。)