构建块将是:将 1 添加到连续组的低位将清除所有带有进位的位,在组上方保留 1 位。例如0xff + 1 = 0x100.
如果任何位未设置,则进位不会一直向上传播,例如0b01101 + 1 = 0b01110,未设置第 4 位。 (并且保留一些现有的设置位未翻转,所以x & (x+1) 将是真的。)
这适用于寄存器底部(加 1)或任何更高位置的位组(使用 (-x) & x 隔离最低设置位并添加,例如使用 BMI1 blsi 或 mov/neg/and )。
一个相关的 bithack 是 y & (y-1) 测试只设置一个位的整数(通过清除最低设置位):https://graphics.stanford.edu/~seander/bithacks.html#DetermineIfPowerOf2。但由于我们使用x+1 生成y,我们可以将其优化为仅x & (x+1) 以检测寄存器底部的连续掩码。
你的具体情况很简单:
- 位范围的底部必须是位 0
- 位范围正好是
n位宽(前缀长度)
这些限制意味着恰好有 1 个整数同时满足这两个要求,因此您应该预先计算它并简单地与cmp/je 比较是否相等。底部设置了n 位的数字是prefix_mask = (1<<n) - 1。通过减法进位(借位)设置该隔离高位以下的所有位,并清除原始位。上面的位保持未设置,因为那个高位满足了借位。
给定前缀长度 n,您可以使用 bts 计算 1<<n(在 Intel CPU 上是单 uop,在 AMD 上是 2 uop,https://agner.org/optimize/)。
;; input: prefix length in EDX
;; output: prefix mask in ESI
xor esi, esi
bts esi, edx ; set bit n; eax |= 1<<n
dec esi ; set all the bits BELOW that power of 2
; then later, inside a loop: input in EAX
cmp eax, esi
je all_bits_set_up_to_prefix
@fuz 为此提出了一个 LUT,但这听起来是个坏主意,即使您必须非常频繁地考虑不同的前缀长度。如果您缺少寄存器,您可以在计算后将其溢出到堆栈内存中,并使用类似cmp [rsp+16], edx 的东西来代替静态LUT,同时以相同的前缀长度循环。
如果您暂时不需要将前缀长度放在寄存器中,则将其溢出到内存中,并保留掩码。
您甚至可以使用lea edx, [eax+1] / bsr edx,edx 将掩码转换回前缀长度,以找到mask+1 的最高设置位的位索引。 (或者,如果前缀长度可以为 32 但不为零,则 bsr / inc.BSR with input=0 保持目标不变,并设置 ZF。AMD 记录了这一点,英特尔的文档说“未定义”,但它们当前的硬件确实未修改目标,这就是该指令对输出具有“错误”依赖性的原因。)
或无需预计算
低n位全1,位#n本身为0的EDX无损测试。(加1清除低位并设置位n如果是这样)。如果以后没有用处,可以使用 inc edx 代替 LEA 进行复制和添加。
;;; check the low n bits, ignoring whether higher bits are set or not
;;; inputs: prefix length in ECX, candidate in EDX
lea eax, [rdx+1]
bt eax, ecx
;;; output: CF = 1 if all the bits from 0..len-1 were set, else 0
如果您还想排除设置任何更高位的可能性,您还需要一条指令,但它可以是一条 test 指令,该指令将与 jcc 进行宏融合,依此类推英特尔 CPU,这不会花费任何额外的微指令。在 AMD CPU 上,btr 是 2 uop,bt 是 1,这需要额外 1 uop。 (test/jcc 可以融合在 AMD Bulldozer 系列及更高版本上。)
;;; input: prefix length in ECX, candidate in EDX
lea eax, [rdx+1] ; produces a single set bit?
btr eax, ecx ; reset that bit, leaving eax=0 if no other bits were set
test eax, eax ; compare against zero
;;; output: ZF=1 (and eax=0) if EDX == (1<<ECX)-1 with no higher bits set.
jz contiguous_bitmask_of_length_ecx
在 Intel 上总共需要 3 uops(在 AMD 上为 4),包括宏融合的 test/jz,在这种情况下进行分支。而且它不会破坏输入寄存器。
我们可以使用x & (x+1) 在寄存器底部检查单个长度未知的连续位组,它会检测是否设置了任何更高位。如果存在未通过进位传播翻转的高位,则 AND 或 TEST 将产生非零结果。
但这会将0 和1 视为像0b0111 这样的多位组。在此测试之前,您可能需要cmp eax, 3 / jb not_multibit_prefix。
; check for a contiguous bit-group at the bottom of a reg of arbitrary length, including 0
;;; input: candidate in EDX
lea eax, [rdx+1] ; carry-out clears all the set bits at the bottom
test eax, edx ; set flags from x & (x+1)
;;; output: ZF=1 if the only set bits in EDX were a contiguous group at the bottom
我查看了lea eax, [rdx+1]/test eax, edx(ZF=1:只设置了连续的低位)/bt eax, ecx(CF=1:它在我们想要的位置结束)的一个奇怪的部分标志黑客。但是 x86 没有要求 CF=1 和 ZF=1 的jcc condition。如果(CF=0 and ZF=0) 则采用ja,如果(CF=1 or ZF=1) 则采用jbe,因此两者都不起作用。当然,这在没有有效的部分标志合并的 CPU 上会很糟糕,导致部分标志停止。
一般情况:位组不必从底部开始。
这排除了简单的预计算。
如上所述,我们可以用(-x) & x 隔离最低设置位。 BMI1 为此添加了一条指令,blsi。因此,如果您可以假设支持 BMI1,则可以在 1 uop 内无损地完成此操作。否则需要 3 个。
unsigned bits_contiguous(unsigned x) {
unsigned lowest_set = (-x) & x; // isolate lowest set bit
unsigned add = x + lowest_set; // carry flips all the contiguous set bits
return (add & x) == 0; // did add+carry leave any bits un-flipped?
}
我把它放在 Godbolt 编译器资源管理器上,看看 gcc 或 clang 是否发现了我没有想到的任何优化。当然,您实际上并不想像我们要求编译器那样实现一个 0 / 1 整数,但是由于他们选择使用 test / setcc 我们可以看看他们做了什么来创建正确的标记条件。
我们可以编写一些测试函数来确保逻辑对于某些编译时常量与#define TEST(n) int test##n(){return bits_contiguous(n);} 是正确的(并查看asm 是xor eax,eax 还是mov eax,1)。 参见C + asm on the Godbolt compiler explorer。 一些有趣的情况是TEST(0) = 1,因为该条件基本上检查是否存在多个位组。 (因此零位组与 1 位组相同,对于此检查。)TEST(0xFFFFFFFF) = 1 也是:拥有x+1 = 0 不是问题。
使用 gcc8.3 -O3,我们得到
# gcc8.3 -O3 -march=haswell (enables BMI1 and BMI2)
bits_contiguous:
blsi eax, edi
add eax, edi
test eax, edi # x & (x+lowest_set)
sete al
movzx eax, al
ret
如果没有 BMI1,我们需要 3 条指令而不是 blsi 的 1 条指令:
mov eax, edi
neg eax
and eax, edi # eax = isolate_lowest(x)
add eax, edi
test eax, edi
为了同时检查位组的特定 长度,@fuz 有一个好主意:popcnt 以确保设置了正确的位数(并单独检查它们是否重新连续)。 Popcnt 不是基准,Nehalem 之前的 CPU 没有它,如果尝试运行它就会出错。
;input: prefix len in ECX, candidate in EDX
;clobbers: EAX
popcnt eax, edx
cmp eax, ecx
jne wrong_number_of_bits_set ; skip the contiguousness test
blsi eax, edi
add eax, edi
test eax, edi # x & (x+lowest_set)
jz contiguous_bitgroup_of_length_ecx
wrong_number_of_bits_set: