采用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 Bit Test and Reset btr eax, 7
完全按照您的要求:清除位7并将CF设置为该位的原始值。
btr eax, 7
或btr reg, imm
在Intel上为1 uop,在AMD上为2 uop。但是,当后面跟随reg, reg
指令时,它无法像jcc
/ test al, 1<<7
那样将宏宏融合到单个比较分支运算中。 (jnz
)。指令数不是性能的唯一因素。良好的ILP和较短的关键路径延迟,尤其是避免不必要的循环承载的依赖链也很重要。但是绝对要考虑在代码中为快速路径计算前端微指令。
x86移位(像大多数ISA一样)将最后一位移出了进位标志。因此,https://agner.org/optimize/设置CF = shr al, 1
并更新AL = orig & 1
。可能有一种方法可以将其与字节移位位相结合,以像orig >> 1
或部分寄存器的技巧一样将它们合并...
由于要处理字节,如果您正在考虑将多个位域组合为寄存器中一个连续的较大整数的方法,则可能需要了解shrd
。
我想以8位值的顺序存储大量数字
我希望您不打算直接使用该格式的数字进行计算。作为紧凑的可变长度序列化格式/编码,对于较小的值,它可以小于Why doesn't GCC use partial registers?,但仍然可以保持int
或在必要时更大,这听起来是合理的。
如果整体速度更重要,请以至少32位的块为单位工作,因此每个CPU操作获得更多的结果位。如此一来,解压缩步骤就更少了,可以组合成一个连续的二进制整数。
一般而言,字节对于BigInteger而言通常是一个错误。请参见uint64_t
上的此codereview答案(不明智地计划将数字存储为ASCII十进制数字的数组)。 32位块中的30个值位可以用于可移植的内容(例如Python在内部使用)。如果您要进行大量的十进制字符串转换,则可以使用10 ^ 9这样的基数;如果不是,则可以使用2 ^ 30。
不使用每条边的所有位,您可以延迟SIMD BigInt class in C++的进位(不归一化)。可以使用1个备用位进行进位,而专用于您的信令策略的最高位用于隐式长度而不是存储长度。 (许多x86 SIMD指令(例如Can long integer routines benefit from SSE?或blendvps
使双字SIMD元素的高位特殊,或者整数SIMD的每个字节的高位都特殊,例如movmskps
,pmovmskb
和pshufb
。因此,您可以获得可以用pblendvb
或bsf
进行位扫描以找到第一个设置位的高位的位掩码。)
bsr
并行位EXTract反序列化此格式pext
在AMD(微编码,不是专用的硬件,甚至在Zen2上)上很慢,并且BMI2并非在所有地方都可用。但是在Intel CPU上是1 uop,3个周期延迟,1 /时钟吞吐量。
请注意,您可以在C语言中使用内在函数来做到这一点。编译器可能不太擅长使用pext
到CSE pext
和btr
进行单个操作,但是即使您编写的是诸如x&(1<<7)
之类的东西而不是内部函数,编译器也应处理此代码。尽管编译器可能会进行恒定传播并为x &= ~(1<<7)
实现反型常量,而不是(~mask) & x
。
以这种格式给出一个指向未知长度数字的指针,最多加载8个字节,并从中提取最多56个值位。 (假设qword加载是安全的:可能会加载垃圾,但不会跨入未映射的页面并出现错误:and
)
andn
[如果没有字节设置高位,则输入全零的Is it safe to read past the end of a buffer within the same page on x86 and x64?将产生全1输出。因此,对于不限数字的情况以及设置输入的高位的情况,我们提取了所有8个字节。
; 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 pmadubsw
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: (rax-1) XOR rax = 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 with this mask will ignore it
pext rax, rcx, rax ; 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
是单Uup单周期等待时间,但是它们在导致blsmsk
的依赖链中,因为在该块中,对于一次迭代而言,没有指令级并行性。它很短,所以如果我们对另外8个字节的数据进行另一次迭代,OoO exec可能会很好地重叠。
如果我们可以并行运行andn
并计算可以在其output
blsmsk
(使用不同的掩码),以将每个字节的高位与pext
所需的高位对齐。或者我们可以pext
找到最低置位的位置,然后以某种方式乘以pext
。该位置是8的倍数,因此我们可以tzcnt并执行blsmsk
或其他操作,然后将该位索引用于BMI2 tzcnt
。如果您有这种格式的数字打包流
,则需要查找下一个数字的起始位置。从7/8
隔离的结束标志模式中,可以依次单击x - (x>>3)
和bzhi
以找到第一个数字结束位的字节索引。您需要添加1以外的内容才能进行过去
。您可以执行bzhi
,但是与rsi
/ rsi = tzcnt(rsi)
相比,它具有额外的延迟,因为它具有3分量LEA(两次rsi >>= 3
操作)。或者如果您向左移动遮罩之前
lea rdi, [rdi + rsi + 1]
,则可以直接执行此操作,此外,它将无终止符视为inc rdi
而不是add rdi, rsi
。+
这项工作可以与blsmsk和pext并行运行。尽管无论如何都要执行
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
。这可能对吞吐量更好,但对延迟更糟:tzcnt
->bzhi
是从RSI在输入准备好blsmsk
之前已经准备好4个周期的延迟,所有这些都在导致[ C0]。与and
/add
仅2个周期。
或者如果要循环直到找到单个数字的末尾(超过8个字节)
,RSI仍保留隔离的信号位。这就是为什么我将tzcnt
用于RSI,而不是RAX。bzhi
或
pext
将CF设置为零,如果其输入为零,那么如果我们可以使用底部的blsmsk
构造循环,则可以将其用作循环分支。它还设置了FLAGS,因此可能需要循环旋转并剥离第一次/以后的迭代
半相关,另一个位打包/拆包问题:and