您可以在一个汇编操作中检查字节上的标志,并检索剩余的7位整数值吗



取一个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.

如果没有,正确的方法是什么?

x86位测试和重置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移位(像大多数ISAs一样)将最后一位移位到进位标志中。因此shr al, 1设置CF=orig & 1并且更新AL=orig >> 1。可能有一种方法可以将其与跨字节移位相结合,以将它们合并,就像使用shrd一样,或者使用部分寄存器技巧。。。

既然你在操纵字节,为什么不;GCC是否使用部分寄存器?如果您正在考虑如何在寄存器中将多个位字段组合为一个连续的较大整数,则可能需要了解这一点。


我想在一个8位值序列中存储大数字

我希望您不打算直接用那种格式的数字进行计算。作为一种紧凑的可变长度序列化格式/编码,这听起来很合理,对于小值,它可以小于int,但仍然可以保持uint64_t,甚至在必要时更大。

如果整体速度更重要,那么就以至少32位的块为单位进行工作,这样每个CPU操作就可以获得更多的结果位。或者说,组合成一个连续的二进制整数的拆包步骤更少。(例如,AVX2变量移位vpsrlvd,将一个双字的顶部与下一个更高元素中的双字底部相邻,然后使用qword变量移位将同样的事情放入128位通道的中间。然后进行字节移位或混洗。

(但是,您可以对具有16位pmullw的16位元素执行此操作如果使用16位块,则乘以2(或1)的幂。或者AVX512BW变量计数或合并掩码16位移位。或者使用pmaddubsw的8位块,甚至使用正确的乘法器组合到SIMD元素的底部:在屏蔽掉信号位之后,用于每对的低字节和高字节的11<<7。)

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

不使用每条肢体的所有比特可以推迟SIMD的进位(而不是归一化):长整数例程能从SSE中受益吗?。这可以使用1个备用比特进行进位,顶部比特专门用于隐式长度的信号策略,而不是存储长度。(许多x86 SIMD指令,如blendvpsmovmskps,会使双字SIMD元素的顶部位变得特殊,或者使整数SIMD(如pmovmskbpshufbpblendvb)的每个字节变得特殊。因此,您可以获得高位的位掩码,您可以使用bsfbsr进行位扫描,以找到第一个设置位。)


打开思路:

如果在整数中选择每个字节的高位=0,选择1表示结束,则可以避免必须清除的杂散设置位。

如前所述,对于SIMD来说,pmaddubsw是将字节组合成16位字的一个不错的选择,具有正确的11<<7乘法器。

然后,使用pmaddwd的另一个步骤可以将单词组合为具有11<<14的双单词,然后设置为AVX2vpsrlv或只是移位和混合。所有这些步骤都采用log2(vector_length)步数,而不是vector_llength步数。

如果没有SIMD,您可以使用x += x作为左移位,但通过执行x += x & mask,使其仅移位一些位。这适用于标量,如果没有SSSE3pmaddubsw,则适用于paddb。(或者使用AVX512BW字节屏蔽的vpaddb,以获得比pmadd更低的延迟。)

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

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


用BMI2pext并行位EXTract反序列化此格式

pext在AMD上运行缓慢(微编码,不是专用硬件,甚至在Zen2上也是如此),并且BMI2并非无处不在。但在英特尔CPU上,它是1个uop,3个周期延迟,1/时钟吞吐量。https://uops.info/

请注意,您可以在C中使用内部函数来执行此操作:英特尔的在线内部函数指南/搜索。(非SIMD标量内部函数在编译器之间的可移植性可能参差不齐,但用于popcnt和BMI等新指令的移植性通常很好。)编译器可能不擅长在单个操作中使用btr到CSEx&(1<<7)x &= ~(1<<7),但它们应该处理此代码,即使您编写的是(~mask) & x而不是内部函数。尽管编译器可能会进行常数传播,并实现and的反向常数,而不是andn

给定一个指向此格式中未知长度数字的指针,加载最多8个字节,并从中提取最多56个值位。(假设qword加载是安全的:可能加载垃圾,但不会进入未映射的页面,错误:在x86和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)或其他操作,然后将该位索引用于BMI2bzhi

如果您有一个这种格式的压缩数字流,您会想找到下一个数字流的起点。从rsi隔离结束标志模式,您可以先rsi = tzcnt(rsi),然后rsi >>= 3来查找数字位第一个结束的字节索引。

你需要再加1才能通过。您可以执行lea rdi, [rdi + rsi + 1],但与inc rdi/add rdi, rsi相比,这具有额外的延迟,因为有三个组件LEA(两个+操作)。

或者,如果在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是从RSI准备好到输入为bzhi准备好的4个延迟周期,所有这些都在通往pext的关键路径中。而CCD_ 76/CCD_。


或者,如果我们想循环直到找到单个数字(超过8个字节)的末尾,RSI仍然保留隔离的信号位。这就是为什么我在RSI中使用andn,而不是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的输入为零,它就会设置CF,所以如果我们可以在底部用blsmsk来构建循环,我们可以将其用作循环分支。它还设置FLAGS,所以也许通过循环旋转和剥离第一次/以后的迭代


BMI2-PEXT部分是一些混杂在一起的随机想法,而不是一个连贯的完全优化的实现。根据您所能做出的任何保证,根据需要进行调整。例如,每个数字8个字节的上限将是有帮助的。

需要注意的一件主要事情是循环携带的dep链(涉及指针增量)的延迟。如果找到下一个数字的开始是高延迟的,那么无序的exec将无法交错许多迭代的工作。


半相关的另一个位封装/拆包问题:将BCD封装到DPD:如何改进amd64组装例程?

最新更新