【问题标题】:Check if a number is even检查一个数字是否是偶数
【发布时间】:2020-09-29 03:13:50
【问题描述】:

我正在通过low level bit hacks 工作,并想为每个人编写一个汇编程序。这是我检查数字是否为偶数的方法:

is_even:
    # check if an integer is even. 
    # This is the same as seeing if its a multiple of two, i.e., & 1<<n - 1
    # rdi stores the number
    xor %eax, %eax
    test $0b1, %rdi
    setz %al
    ret

_start:
    mov $5, %rdi
    call is_even

有什么方法可以改进上述内容或使其更具可读性吗?是否可以使用 2 条指令而不是 3 条指令进行 is_even 检查,因为第一个 xor 和第二个 setz 似乎可能会转换为一个。

【问题讨论】:

    标签: assembly x86 bit-manipulation x86-64


    【解决方案1】:

    TL:DR:加 1 翻转低位,保证,所以你可以使用lea/and。见下文。


    您选择编写一个返回布尔整数的整个函数,而不是仅仅创建一个 FLAGS 条件(这是大多数代码所需要的:test $1, %dil 并且您已经完成;分支或 cmov 或 setnz 或 setz 或任何您实际想要做一个基于偶数的值)。

    如果您要返回一个整数,那么您实际上不需要将条件放入 FLAGS 并退出,特别是如果您想要一个“宽”返回值。 x86 setcc 仅写入低字节是一种不方便的设计,大多数情况下需要额外的异或归零指令来创建更宽的 0 / 1 整数。 (我希望 AMD64 已经整理了设计并将 64 位模式的操作码的含义更改为 setcc r/m32,但他们没有。)

    您选择了函数的语义以返回 1 for even;这与低位的值相反。 (即return (~x)&amp;1;)您还选择使用 x86-64 System V 调用约定创建一个函数,这会增加调用约定的开销,该调用约定将 arg 放入与您传入的寄存器不同的寄存器中。

    这个函数显然太琐碎了,不值得调用/返回开销;在现实生活中,您只需将其内联并优化到调用者中。因此将其优化为作为独立函数主要是一个愚蠢的练习,除了获得 0/ 的想法1 在与原始文件不同的寄存器中,而不破坏它。

    如果我在https://codegolf.stackexchange.com/ 上写答案,我会按照this code-golf tip 并选择我的调用约定在EAX 中传递一个arg 并在AL 中返回一个布尔值(就像gcc -m32 -mregparm=3 一样)。或者在 ZF 中返回一个 FLAGS 条件。或者,如果允许,请选择我的返回语义,使 AL=0 表示偶数,AL=1 表示奇数。那么

    # gcc 32-bit regparm calling convention
    is_even:          # input in RAX, bool return value in AL
        not   %eax             # 2 bytes
        and   $1, %al          # 2 bytes
        ret
    
    # custom calling convention:
    is_even:   # input in RDI
               # returns in ZF.  ZF=1 means even
        test  $1, %dil         # 4 bytes.  Would be 2 for AL, 3 for DL or CL (or BL)
        ret
    

    2 条指令而不破坏输入

    is_even:
        lea   1(%rdi), %eax          # flip the low bit
        and   $1, %eax               # and isolate
        ret
    

    XOR 是不带进位的加法。 当进位为零时(除了 ADC 外,保证低位),给定位的结果与 XOR 和加法相同。检查 1 位“half adder”(无进位)的真值表/门等效项:“sum”输出实际上只是 XOR,进位输出只是 AND。

    (XOR 与 1 翻转一点,与 NOT 相同。)

    在这种情况下,我们不关心进位或任何高位(因为我们即将用&amp; 1 核对这些位是相同的操作),所以我们可以使用 LEA 作为复制并添加翻转低位。

    使用 XOR 而不是 ADD 或 SUB 对于 SIMD 很有用,其中 pxor 可以在比 Skylake 之前的 CPU 上的 paddbpsubb 更多的端口上运行。当你想将pcmpgtb 的无符号范围转换为有符号时,你想添加-128,但这与翻转每个字节的高位是一样的。


    您可以使用它来翻转更高的位,例如lea 8(%rdi), %eax 将翻转 1&lt;&lt;3 位位置(并可能进入所有更高位)。我们知道该位的进位将为零,因为x + 0 不进位,并且8 的低 3 位都是 0。

    (这个想法是后来 https://catonmat.net/low-level-bit-hacks 中一些更有趣的 bit-hacks 的核心)

    【讨论】:

    • 太简洁了,感谢您解释 lea 技巧。从另一个答案来看,为什么编译器不这样做呢? godbolt.org/z/xPGYfb.
    • @David542:显然没有人教过 GCC 或 clang 的优化器来寻找可能的优化。它只与 x86 后端相关(LEA 是特殊的),而不是与大多数优化发生的编译器的架构中立部分相关。大多数可以复制和添加的 ISA 也可以复制和异或或复制而不是复制。如果我不解决,您可以提交 GCC 和 clang 的错过优化错误。
    【解决方案2】:

    我不能把它归结为两条指令,但我可以把它打得更短一些。

    您当前的版本是 12 个字节,包括 ret。您可以使用 test $1, %dil 来减少两个字节,因为输入的高字节无关紧要,因此将 4 字节立即数换成 1 字节立即数和前缀字节。这使它下降到 10。

    您可以利用移位指令转移到进位标志这一有点模糊的事实,并执行以下操作,再减少两个字节

    is_even: // 8 bytes
        xor %eax, %eax
        shr $1, %edi
        setnc %al
        ret
    

    gcc 和 clang both do

    is_even: // 8 bytes
        mov %edi, %eax
        not %eax
        and $1, %eax
        ret
    

    少一个字节,有

    is_even: // 7 bytes
        shr $1, %edi
        sbb %eax, %eax
        inc %eax
        ret
    

    sbb 是“借位减法”,它从另一个寄存器中减去一个寄存器,如果设置了进位标志,则再减去 1。如果输入为偶数,则为 0,如果为奇数,则为 -1。然后加 1 让我们到达我们想要的位置。这可能会更慢,因为我不确定 CPU 是否知道结果不依赖于 %eax 的先前值。

    不过,我看不出有什么方法可以简化为两条指令。这是条件setcc 指令的一个令人讨厌的功能,它们只设置低字节而将寄存器的其余部分单独放置,在您希望布尔值在完整寄存器中的常见情况下,迫使您自己将其归零。而且我们必须在两个不同的寄存器中获取输入和输出,这很尴尬,因为 x86 的模型中输出寄存器始终是输入之一。

    【讨论】:

    • 保存 1 个字节的更理智的方法(避免 test r32, imm32,因为没有 test r32, imm8)是 test $1, %dil。但这仍然需要一个 REX 前缀和立即,不像你的班次。与使用 48 f7 c7 01 00 00 00 test rdi,0x1 而不是简单的 40 f6 c7 01 test dil,0x1 的 OP 的原始版本相比,您实际上节省了 5 个字节
    • 我发布了一个答案,使用 LEA 复制和翻转低位的 2 指令方式。
    猜你喜欢
    • 2015-05-31
    • 1970-01-01
    • 2023-01-14
    • 2015-09-15
    • 2011-11-12
    • 2010-12-29
    • 2023-02-05
    • 1970-01-01
    相关资源
    最近更新 更多