assembly 你能在一次汇编操作中检查一个字节的标志,并检索剩余的7位整数值吗?

xoshrz7s  于 2023-06-23  发布在  其他
关注(0)|答案(1)|浏览(82)

取一个8位、16位、32位或64位的数字,从中提取第一位,检查该位是否为真,同时在删除前导位后存储结果数字的最佳方法(最少/最快操作)是什么?(在组装中)。

integerInBitsWithLeadingFlag = 10001000
flag == 1
integer == 0001000 = 1000

在汇编语言中,我知道这里和那里有一些技巧来划分和保留余数,在结果中存储两个变量,或者其他类似的事情。也许有一些方法可以在组装中做到这一点。
我问这个问题的原因是因为我想在一个8位值的序列中存储大数,其中前导位是指示是否应该将“更多”值连接在一起的标志,其余的7位用于计算最终的整数/bigint。如果最好将标志存储在最后一位/尾随位,那么最好包括:)
我是新来的,所以我不知道如何才能做到这一点。

; assembly pseudocode
start:
  AND rax, 10000000 ; AND the value with a leading 1 (or something like this)
  CMP rax, 1 ; compare the leading value with 1 to see if it matches.
  JE matches
  JNE notmatches

matches:
  ; remove bit from rax to get integer value.

notmatches:
  ; same, remove bit from rax to get integer value.

有什么我可以这样做的吗?

start:
  ANDFLAGWITHREMAINDER rax, 10000000
  ; and now, after 1 operation,
  ; you have the flag and the integer.

如果不是,那么什么是正确的方法呢?

pes8fvy9

pes8fvy91#

x86 Bit Test and Reset btr eax, 7完全符合您的要求:清除位7并设置CF =该位的原始值。
btr reg, immreg, reg在Intel上是1 uop,在AMD上是2 uop。但是,当后面跟着jcc指令时,它不能像test al, 1<<7/jnz那样宏融合到单个比较和分支uop中。(https://agner.org/optimize/)。* * 指令数不是影响性能的唯一因素**。良好的ILP和短的关键路径延迟,特别是避免不必要的循环依赖链,也很重要。但是计算代码中快速路径的前端uop绝对是需要考虑的事情。
x86移位(与大多数ISA一样)将移出的最后一位放入进位标志。因此,shr al, 1设置CF = orig & 1并更新AL = orig >> 1。可能有一种方法可以将其与跨字节的位移位相结合,以合并它们,如shrd,或部分寄存器技巧......
由于您正在操作字节,如果您正在考虑如何将多个位域组合成寄存器中的一个连续较大整数,您可能需要了解Why doesn't GCC use partial registers?
我想在一个8位值的序列中存储大数
我希望你不是打算直接用那种格式的数字来计算。这听起来很合理,因为它是一种紧凑的可变长度序列化格式/编码,对于较小的值,它可以小于int,但仍然可以支持uint64_t,甚至在必要时更大。
如果整体速度更重要,那么至少要在32位的块中工作,这样每次CPU操作都可以获得更多的结果位。因此,组合成单个连续二进制整数的解包步骤更少。(例如,AVX2变量移位vpsrlvd将一个双字的顶部与下一个更高元素中的双字的底部相邻,然后使用qword变量移位在128位通道的中间做同样的事情。然后是字节移位或混洗。
(但是,如果使用16位块,则可以使用16位pmullw乘以2(或1)的幂来对16位元素执行此操作。或AVX512BW可变计数或合并屏蔽16位移位。或者8位的块与pmaddubsw甚至组合到SIMD元素的底部,并使用正确的乘数:11<<7用于每对的低和高字节,在屏蔽掉信号位之后。)

    • 字节通常是BigInteger的错误**。请参阅BigInt class in C++上的这个代码审查答案(它不明智地计划将数字存储为ASCII十进制数字数组)。32位块中的30个值位对于可移植的东西来说是很好的(就像Python在内部使用的那样)。如果你做了大量的十进制字符串转换,你可以使用像10^9这样的基数,如果没有的话,可以使用2^30。

不使用每个分支的所有位允许您延迟SIMD的进位(不归一化):Can long integer routines benefit from SSE?。这可以使用1个备用位进行进位,顶部位专用于隐式长度而不是存储长度的信令策略。(许多x86 SIMD指令(如blendvpsmovmskps)使双字SIMD元素的顶部位特殊,或使整数SIMD(如pmovmskbpshufbpblendvb)的每个字节特殊。因此,您可以获得高位的位掩码,您可以使用bsfbsr进行位扫描以找到第一个设置位。)

解包思路:

如果在整数中选择每个字节的高位= 0,并选择1表示结束,则可以避免必须清除的杂散设置位。
如前所述,pmaddubsw是SIMD将字节组合成16位字的理想选择,具有正确的11<<7乘法器。
然后使用pmaddwd的另一个步骤可以将字与11<<14组合为双字,然后您设置为AVX2 vpsrlv或只是移位和混合。然后,所有这些都需要log2(vector_length)步数,而不是vector_length步数。
如果没有SIMD,您可以使用x += x作为左移,但通过执行x += x & mask使其仅移动 * 一些 * 位。这适用于标量,如果没有SSSE3 pmaddubsw,则适用于paddb。(或者使用AVX512BW字节掩码vpaddb,以获得比pmdd更低的延迟。)

x = ... (bits) 0abcdefg 0ABCDEFG
   +  (hex) x & 0x...00FF00FF
                  0abcdefg ABCDEFG0

这将为您提供包含14个值位的连续16位块。但是,这次每个块由 * 2 * 0位分隔,因此掩码加法不是最有效的方法。可能从那里开始,与0xFFFF0000FFFF0000进行AND运算,然后将其右移2,然后以另一种方式屏蔽原始数据,并与OR混合。

使用BMI2 pext并行位EXTract反序列化此格式

pext在AMD上很慢(微编码,不是专用硬件,即使在Zen2上也是如此),BMI2也不是到处都可用。但在Intel CPU上,它是1 uop,3周期延迟,1/时钟吞吐量。https://uops.info/

请注意,您可以在C中使用intrinsic:Intel的online intrinsics guide / search(非SIMD标量intrinsic在编译器之间的可移植性可能不太好,但对于像popcnt和BMI这样的新指令来说,移植性一般都很好。)编译器可能不擅长将btr用于CSE x&(1<<7)x &= ~(1<<7)到单个操作中,但它们应该处理这些代码,即使你编写了像(~mask) & x这样的东西而不是intrinsic。尽管编译器可能会执行常量传播并具体化and的反转常量,而不是执行andn

给定一个指向这种格式的未知长度数字的指针,加载最多8个字节,并从中提取最多56个值位。(假设qword加载是安全的:可能会加载垃圾,但不会进入未Map的页面和错误:Is it safe to read past the end of a buffer within the same page on x86 and x64?

; input pointer in RDI: a set bit 7 indicates end of number
; clobbers RCX, RDX
; result in RAX
;; great on Intel, probably can do better on AMD one byte at a time or with SIMD pmaddubsw 

    mov    rcx, [rdi]                     ; 8 bytes, including possible trailing garbage
    mov    rdx, 0x7F7F7F7F7F7F7F7F
    andn   rsi, rdx, rcx                  ; isolate high bits: (~rdx) & rcx

    blsmsk rax, rsi                       ; get mask up to lowest set bit: (rsi-1) XOR rsi = mask up to (and including) the first signal bit
    and    rax, rcx                       ; clear high garbage
       ; RAX = 0 above the number.  The end-of-number flag is still set but pext only grabs bits 6:0 from each byte.

    pext   rax, rax, rdx                  ; rax = 8x 7-bit fields packed down to low 56
   ; uint64_t result in RAX, from the first 1 to 8 bytes of [rdi],
   ; depending on where the first number-end flag was

如果没有字节设置高位,则blsmsk与全零输入产生全一输出。因此,对于数字未结束的情况以及输入的顶部位被设置的情况,我们提取所有8个字节。
andnblsmsk是单uop单周期延迟,但它们位于导致pext的依赖链中,因为在一个迭代的一个块中没有指令级并行。它非常短,所以如果我们在另外8个字节的数据上进行另一次迭代,OoO exec可以很好地重叠。
如果我们可以并行运行pext,并计算一个掩码,我们可以在它的 * 输出 * 而不是输入上使用,那就太酷了。但7:8的比例是个问题。我们可以并行运行pext两次(使用不同的掩码),以便将每个字节的高位与blsmsk所需的位置对齐。或者我们可以用tzcnt来找到最低设置位的位置,然后乘以7/8。位置是8的倍数,因此我们可以tzcnt并执行x - (x>>3)或其他操作,然后将该位索引用于BMI 2 bzhi

如果你有一个这种格式的打包数字流,你会想找到下一个开始的地方。从rsi isolated end-flag模式中,您可以先rsi = tzcnt(rsi),然后rsi >>= 3来查找第一个数字结束位的字节索引。

你需要再加1才能超过这个值。您可以执行lea rdi, [rdi + rsi + 1],但与inc rdi/add rdi, rsi相比,由于3组件莱亚(两个+操作),因此具有额外的延迟。
或者,如果你左移**tzcnt之前的掩码 *,你可以直接这样做,作为奖励,它会将无终止符视为8而不是9

add   rsi, rsi               ; left-shift the end-of-number flags to the bottom of the next byte (or out)
    tzcnt rsi, rsi               ; rsi = bit-index of first bit of next number, 64 if RSI=0

 ; shrx  rcx, rcx, rsi           ; shift to the bottom and restart instead of reloading

    shr   esi, 3                 ; bit index -> byte index.  We know it's a small integer so 32-bit operand-size is fine and more saves code-size
    add   rdi, rsi               ; advance the pointer

这项工作可以与blsmsk和pext并行运行。但是如果我们无论如何都要做tzcnt的工作,也许我们应该使用bzhi而不是blsmsk/and。这可能对吞吐量更好,但对延迟更差:add-> tzcnt是在bzhi的输入准备就绪之前从RSI准备就绪的4个周期的延迟,所有这些都在通往pext的关键路径中。而blsmsk/and仅为2个周期。

或者,如果我们想循环直到找到一个数字(超过8个字节)的结尾,RSI仍然保存隔离的信号位。这就是为什么我将andn转换为RSI,而不是RAX。

... continuing from above to keep going until the end of large number
    ... do something with the 56-bit RAX chunks, like overlapping stores into memory?
    ; rsi still holds the isolated signal bits
    add    rdi, 8
    test   rsi, rsi
    jnz   .loop                     ; }while(end of number not found)

或者如果blsmsk的输入为零,则blsmsk设置CF,因此如果我们可以在底部使用blsmsk构建循环,我们可以将其用作循环分支。它还设置了FLAGS,因此可能会使用循环旋转和剥离第一次/以后的迭代
BMI 2 PEXT部分是一些随机的想法混杂在一起,而不是一个连贯的完全优化的实现。根据您可以做出的任何保证,根据需要进行调整。例如,每个数字8字节的上限将是有帮助的。
要注意的一个主要问题是循环携带的dep链的延迟,它涉及指针增量。如果查找下一个数字的开始是高延迟的,无序的exec将无法交错许多迭代的工作。
半相关的,另一个位打包/解包问题:Packing BCD to DPD: How to improve this amd64 assembly routine?

相关问题