检查数字是否为偶数

问题描述

我正在逐步研究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似乎可能被转换为一个

解决方法

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


您选择编写一个返回布尔整数的完整函数,而不仅仅是创建FLAGS条件(大多数代码需要:test $1,%dil,然后完成;分支或cmov或setnz或setz或其他任何操作您实际上是想以偶数为准)。

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

您选择了函数的语义以偶返回1;这与低位的值相反。 (即return (~x)&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。

(与1异或的XOR与NOT相同)。

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

对于SIMD,使用XOR而不是ADD或SUB非常有用,其中在Skylake之前,pxor可以在CPU上运行的端口数量超过paddbpsubb。如果您想对pcmpgtb或其他内容进行无符号范围转换,则要添加-128,但这与翻转每个字节的高位一样。


您可以使用它来翻转更高的位,例如lea 8(%rdi),%eax将翻转1<<3位的位置(并可能携带到所有更高的位中)。我们知道该位的进位为零,因为x + 0不进位,并且8的低3位全为0。

(此思想对于https://catonmat.net/low-level-bit-hacks中后来出现的一些更有趣的位hacks至关重要)

,

我无法将其简化为两个说明,但是我可以将它打短一点。

您当前的版本是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的模型,输出寄存器始终是输入之一,所以这很尴尬。