取一个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.
如果不是,那么什么是正确的方法呢?
1条答案
按热度按时间pes8fvy91#
x86 Bit Test and Reset
btr eax, 7
完全符合您的要求:清除位7并设置CF =该位的原始值。btr reg, imm
或reg, 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元素的底部,并使用正确的乘数:1
和1<<7
用于每对的低和高字节,在屏蔽掉信号位之后。)不使用每个分支的所有位允许您延迟SIMD的进位(不归一化):Can long integer routines benefit from SSE?。这可以使用1个备用位进行进位,顶部位专用于隐式长度而不是存储长度的信令策略。(许多x86 SIMD指令(如
blendvps
或movmskps
)使双字SIMD元素的顶部位特殊,或使整数SIMD(如pmovmskb
、pshufb
和pblendvb
)的每个字节特殊。因此,您可以获得高位的位掩码,您可以使用bsf
或bsr
进行位扫描以找到第一个设置位。)解包思路:
如果在整数中选择每个字节的高位= 0,并选择1表示结束,则可以避免必须清除的杂散设置位。
如前所述,
pmaddubsw
是SIMD将字节组合成16位字的理想选择,具有正确的1
和1<<7
乘法器。然后使用
pmaddwd
的另一个步骤可以将字与1
,1<<14
组合为双字,然后您设置为AVX2vpsrlv
或只是移位和混合。然后,所有这些都需要log2(vector_length)步数,而不是vector_length步数。如果没有SIMD,您可以使用
x += x
作为左移,但通过执行x += x & mask
使其仅移动 * 一些 * 位。这适用于标量,如果没有SSSE3pmaddubsw
,则适用于paddb
。(或者使用AVX512BW字节掩码vpaddb
,以获得比pmdd更低的延迟。)这将为您提供包含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
用于CSEx&(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?)
如果没有字节设置高位,则
blsmsk
与全零输入产生全一输出。因此,对于数字未结束的情况以及输入的顶部位被设置的情况,我们提取所有8个字节。andn
和blsmsk
是单uop单周期延迟,但它们位于导致pext
的依赖链中,因为在一个迭代的一个块中没有指令级并行。它非常短,所以如果我们在另外8个字节的数据上进行另一次迭代,OoO exec可以很好地重叠。如果我们可以并行运行
pext
,并计算一个掩码,我们可以在它的 * 输出 * 而不是输入上使用,那就太酷了。但7:8的比例是个问题。我们可以并行运行pext
两次(使用不同的掩码),以便将每个字节的高位与blsmsk
所需的位置对齐。或者我们可以用tzcnt
来找到最低设置位的位置,然后乘以7/8
。位置是8的倍数,因此我们可以tzcnt并执行x - (x>>3)
或其他操作,然后将该位索引用于BMI 2bzhi
。如果你有一个这种格式的打包数字流,你会想找到下一个开始的地方。从
rsi
isolated end-flag模式中,您可以先rsi = tzcnt(rsi)
,然后rsi >>= 3
来查找第一个数字结束位的字节索引。你需要再加1才能超过这个值。您可以执行
lea rdi, [rdi + rsi + 1]
,但与inc rdi
/add rdi, rsi
相比,由于3组件莱亚(两个+
操作),因此具有额外的延迟。或者,如果你左移**
tzcnt
之前的掩码 *,你可以直接这样做,作为奖励,它会将无终止符视为8
而不是9
。这项工作可以与blsmsk和pext并行运行。但是如果我们无论如何都要做
tzcnt
的工作,也许我们应该使用bzhi
而不是blsmsk
/and
。这可能对吞吐量更好,但对延迟更差:add
->tzcnt
是在bzhi
的输入准备就绪之前从RSI准备就绪的4个周期的延迟,所有这些都在通往pext
的关键路径中。而blsmsk
/and
仅为2个周期。或者,如果我们想循环直到找到一个数字(超过8个字节)的结尾,RSI仍然保存隔离的信号位。这就是为什么我将
andn
转换为RSI,而不是RAX。或者如果
blsmsk
的输入为零,则blsmsk
设置CF,因此如果我们可以在底部使用blsmsk
构建循环,我们可以将其用作循环分支。它还设置了FLAGS,因此可能会使用循环旋转和剥离第一次/以后的迭代BMI 2 PEXT部分是一些随机的想法混杂在一起,而不是一个连贯的完全优化的实现。根据您可以做出的任何保证,根据需要进行调整。例如,每个数字8字节的上限将是有帮助的。
要注意的一个主要问题是循环携带的dep链的延迟,它涉及指针增量。如果查找下一个数字的开始是高延迟的,无序的exec将无法交错许多迭代的工作。
半相关的,另一个位打包/解包问题:Packing BCD to DPD: How to improve this amd64 assembly routine?