如何在Y86-64(或其他具有ADD + AND但没有本机右移的玩具ISA)中执行右移

时间:2019-04-05 16:17:24

标签: assembly bit-manipulation bit-shift y86

我正在尝试在Y86-64上执行右移

要进行左移,我知道我需要乘以2 ^ n,其中n是我们想要的位移数,例如,如果我们想移4,则为2 ^ 4 = 16并进行加法循环对其进行乘法运算,但是我不确定该如何进行右移,我想我需要进行除法运算,但是不确定如何进行运算

pcount_do:
     movq $0, %rax
.L2: movq %rdi, %rdx
     shrq %rdi
     ret

2 个答案:

答案 0 :(得分:1)

鉴于Y86的指令集缺少移位和除法,我将寻求与该C代码等效的东西:

uint64_t lut[] = {
    1,
    2,
    4,
    8,
    16,
    32,
    64,
    128,
    256,
    512,
    1024,
    2048,
    4096,
    8192,
    16384,
    32768,
    65536,
    131072,
    262144,
    524288,
    1048576,
    2097152,
    4194304,
    8388608,
    16777216,
    33554432,
    67108864,
    134217728,
    268435456,
    536870912,
    1073741824,
    2147483648,
    4294967296,
    8589934592,
    17179869184,
    34359738368,
    68719476736,
    137438953472,
    274877906944,
    549755813888,
    1099511627776,
    2199023255552,
    4398046511104,
    8796093022208,
    17592186044416,
    35184372088832,
    70368744177664,
    140737488355328,
    281474976710656,
    562949953421312,
    1125899906842624,
    2251799813685248,
    4503599627370496,
    9007199254740992,
    18014398509481984,
    36028797018963968,
    72057594037927936,
    144115188075855872,
    288230376151711744,
    576460752303423488,
    1152921504606846976,
    2305843009213693952,
    4611686018427387904,
    9223372036854775808};

uint64_t rshift(uint64_t source, int amount) {
    uint64_t result = 0;
    for(int i = amount; i < 64; ++i) {
        if(source & lut[i]) result |= lut[i-amount];
    }
    return result;
}

这只需添加/订阅/和/或加上查找表就可以完成。

如果我们想变得更聪明,就像@PeterCordes建议的那样,我们可以使用8个条目的查找表并处理整个字节,但是与遍历每个位相比,这需要更多的簿记工作。

---更新---

@PetreCordes正确地指出,查找表实际上是无用的,因为我正在对位进行循环,因此使用总和来计算下一个2的次幂是不重要的:

uint64_t rshift(uint64_t source, int amount) {
    uint64_t result = 0;
    uint64_t read_bit = 1;
    uint64_t write_bit = 1;
    for(int i = 0; i < amount; ++i) read_bit = read_bit + read_bit;
    for(int i = amount; i < 64; ++i) {
        if(source & read_bit) result |= write_bit;
        read_bit = read_bit + read_bit;
        write_bit = write_bit + write_bit;
    }
    return result;
}

答案 1 :(得分:1)

就像Matteo所示,您可以一次循环一位,在一个位置读取并在另一位置写入位。

Matteo的答案是通过移动一个掩码并在一个锁步移动的位置(从寄存器的底部开始)(移动另一个掩码)来在可变位置读取的。

读取输入的MSB更容易,然后使用add same,same将输入左移,然后重复输入。因此,我们从最高位开始读取位,并从其MSB开始构造结果。 (我们一次向目标位置左移了1位,而ADD向左移,并且有条件加法设置是否设置新的位。)

我们可以使用2的补码符号比较来读取寄存器的高位。如果设置了x < 0,则没有设置。

x86和y86具有一个称为SF的标志,该标志是根据(ALU操作的)结果的MSB设置的。 x86具有js / cmovs / sets指令,这些指令直接检查SF条件。 y86仅具有jl / jge和其他检查SF!=OF的带符号比较条件,因此我们需要对零进行额外比较以清除OF(x - 0溢出)。

或者在语义上,实际上与零进行比较,而不仅仅是读取SF。 (除我们can optimize compare-against-zero into andl %eax,%eax or andq %rax,%rax以外,如果您使用的是不具有次中间效果的y86版本,这将非常有帮助。y86还缺少x86的非破坏性testcmp指令,类似于andsub,但只写标志。)

https://www.simn.me/js-y86/上测试过的32位y86版本

移植到y86-64应该很简单。 (更改注册表名称,然后32变为64)。
  测试案例:0x12345 >> 1 = 0x000091a2。 (我没有找到一种方法来永久链接该站点上的代码,就像Godbolt编译器浏览器所允许的那样。)

   # constant input test case
    irmovl  0x12345, %eax
    #  irmovl  3, %ecx           # this could trivial handle variable counts, but doesn't.
# start of right-shift block:
# input: EAX = number to be shifted
# output: EDX =  number >> 1
# clobbers: EAX, ECX, EDI.   (EDI=1 to work around lack of add-immediate)

    xorl    %edx, %edx      # dst = 0.   like # irmovl  $0, %edx
    irmovl  1, %edi         # y86 is missing immediate add?

# shift 32-n bits from EAX into the bottom of EDX
# one at a time using SF to read them from the MSB
    irmovl  31, %ecx        # hard code count = 32 - 31
                            # or calculate this as 32 - count with neg / add or equivalent
rshift:                    # do {
    addl   %edx, %edx       # dst <<= 1

    andl   %eax, %eax       # compare against zero because y86 is missing js / cmovs that tests just SF
    jge   MSB_zero          # jge = jnl = not lower
    xorl    %edi,  %edx      # edx ^= 1.   y86 is missing OR?  low bit = 0 so we can ADD or XOR to set it
  MSB_zero:

    addl   %eax, %eax       # src <<= 1

    subl   %edi, %ecx
    jne   rshift            # }while(--ecx);  # semantically jnz


    halt # result in EDX
    #shr    $1, %eax

我使用了Xor调零功能,因为y86仿真器可以汇编成可变长度的机器代码,例如x86。 (因此irmovl 0, %edx的效率较低)。


或者使用CMOVL从EAX的MSB到EDX的LSB进行无分支

# loop body:
    addl       %edx, %edx      # dst <<= 1

    xorl       %esi, %esi      # esi = 0
    sub        %esi, %eax      # src -= 0  to set flags
    cmovl      %edi, %esi      # esi = (src<0) ? 1 : 0  = MSB of EAX
    addl       %esi, %edx      # copy the bit into EDX  (can't carry to higher bits)

    addl       %eax, %eax      # src <<= 1

如果您的y86模拟器模拟了分支错误预测的性能损失,请使用此功能。否则,分支将减少指令。


或者,如果您关心性能,应该可以一次对整个字节使用查找表,并且跨字节边界进行修复。

但是由于没有左移来有效地组装单独的字节,因此对于每个字节位置,您都需要一个单独的256项qword LUT!或从偏移量加载,然后屏蔽掉“垃圾”字节。

哦,您需要右移以从qword中提取字节以提供数组索引。如果y86可以进行字节加载,则可以将输入整数存储到内存中,然后一次将其重新加载1个字节。或再次使用未对齐的qword负载模拟字节负载,并使用0x00...0FF与AND进行模拟,以在寄存器底部隔离该字节。


实际上,我们可以使用具有字节偏移量和掩码的存储/重载来仅在少数几条指令中“有效地”进行8位倍数的右移。

糟糕,但是对于运行时变量计数,我们遇到了鸡/蛋问题。我们需要count / 8作为字节偏移量,因为一个字节中有8位。但是计数很小,因此我们可以使用重复减法循环。 (您可能希望使用0x3f或0x1f AND(取决于操作数大小)来将计数换行为64或32,就像x86硬件移位一样。这将避免索引过大而超出正确范围的索引存储方式)

无论如何,您可以通过将向上舍入(移出太多位),然后将其扩展为处理不是8的倍数的右移计数像问题第一部分中的循环一样,将所需的位一次返回。 (在未对齐的加载之后,将那些位放在寄存器的顶部。)

或者也许使用Matteo的走面具的方法,使用LUT作为起点。但是,如果我们已经在进行存储/未对齐的重装以进行字节移位,则另一个重装可能很好。我们可以计算出相对于第一次未对齐重载的正确偏移量,即之前的4或8个字节,因此起始MSB恰好位于第一次加载的最低位之下。