测试所有真实位是否相邻

时间:2019-03-24 18:32:00

标签: assembly optimization bit-manipulation x86-64 masm

我正在遍历树来构建霍夫曼前缀代码LUT。我正在使用寄存器来跟踪当前前缀。

我退出分析树的算法的条件使用以下条件,该条件必须为真:

  1. 树中的当前元素必须是叶子

  2. 当前前缀代码的所有位均已设置,没有错误位。

对于第二种情况,我还要跟踪前缀字符串的当前长度(以位为单位)。

如何测试一个寄存器的位设置是否超过1位,并且所有设置位彼此相邻?

编辑:设置的位组必须从位0开始并且与前缀长度(存储在另一个寄存器中)一样长

2 个答案:

答案 0 :(得分:2)

为此的构造块将是:在连续组的低位加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。但是,由于我们使用y生成x+1,因此我们可以将其优化到x & (x+1),以检测寄存器底部的连续掩码。


您的具体情况很简单:

  • 位范围的底部必须为位0
  • 位范围正好n位宽(前缀长度)

这些限制意味着完全符合1个要求的整数,因此您应该对其进行预先计算,并简单地与cmp/je 进行比较。在底部设置n位的数字是prefix_mask = (1<<n) - 1。减法的进位(借位)将所有位设置为低于该隔离的高位,并清除原始位。高于该位的位保持不变,因为该高位满足了借入要求。

鉴于前缀长度n,您可以使用1<<n(在Intel CPU上为单-uop,在AMD CPU上为2 uop,{{3 }})

bts

@fuz为此提出了一个LUT,但这听起来是个坏主意,即使您不得不非常频繁地考虑不同的前缀长度。如果寄存器不足,则可以在计算后将其溢出到堆栈内存中,并使用;; 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 之类的东西代替静态LUT,同时以相同的前缀长度循环。

或者如果您暂时不需要寄存器中的前缀长度,则将其溢出到内存中,只需保留掩码即可。

您甚至可以使用cmp [rsp+16], edx / lea edx, [eax+1]将掩码转换回前缀长度,以找到bsr edx,edx的最高设置位的位索引。 (或者,如果前缀长度可以为32,但不能为零,则mask+1 / bsrhttps://agner.org/optimize/的input = 0会使目标保持不变,并设置ZF。AMD文档为此,Intel的文档说“未定义”,但他们当前的硬件确实使目标保持不变,这就是指令对输出具有“假”依赖性的原因。)


或者不进行预计算

EDX的低inc位全为1,且位#n本身为0。(加1清除低位并将n位设置为 if < / em>)。如果之后没有用处,可以使用n代替LEA进行复制和添加。

inc edx

如果您还想排除设置的更高位,您还需要一条指令,但这可以是一条;;; 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 指令,它将与{{1} },因此在Intel CPU上不需要花费额外的成本。在test为2 oups,而jcc为1的AMD CPU上,这需要多花费1 uop。 (test / jcc可以在AMD Bulldozer系列及更高版本上融合。)

btr

要在这种情况下分支,英特尔(包括宏融合测试/ jz)在英特尔(AMD为4)上总共要花费3 uops 。而且不会破坏输入寄存器。


我们可以使用bt 在寄存器的底部检查未知长度的单个连续位组,该组确实会检测是否设置了更高的位。如果有一个高位没有被进位传播所翻转,则AND或TEST会产生非零的结果。

但这将;;; 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 x & (x+1)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 (ZF = 1:仅设置了连续的低位)/ lea eax, [rdx+1](CF = 1:它结束于我们想要的位置)。但是x86没有一个BSR,它需要CF = 1 ZF = 1。如果test eax, edx使用bt eax, ecx,如果ja使用(CF=0 and ZF=0),那么两者都不起作用。当然,如果没有有效的部分标志合并,这在CPU上将是可怕的,从而导致部分标志停顿。


一般情况:位组不必从底部开始。

这排除了简单的预计算。

如上所述,我们可以用jbe隔离最低的设置位。 BMI1为此添加了一条指令(CF=1 or ZF=1) 。因此,如果可以假设支持BMI1,则可以1 uop进行无损检测。否则需要3。

(-x) & x

我将其放在Godbolt编译器浏览器上,以查看gcc或clang是否发现了我没有想到的任何优化。当然,您实际上并不想像我们要求编译器那样具体化0/1整数,但是由于他们选择使用blsi / 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? } ,因此我们可以看看它们的作用创建正确的标志条件。

我们可以编写一些测试函数,以确保使用test的某些编译时常量的逻辑正确(并查看asm是setcc还是#define TEST(n) int test##n(){return bits_contiguous(n);})。 请参见jcc condition一些有趣的情况是xor eax,eax = 1,因为该条件基本上检查是否存在多个位组。 (因此,对于此检查,零位组与1位组相同。)mov eax,1也等于1:拥有TEST(0)没问题。

有了gcc8.3 -O3,我们得到了

TEST(0xFFFFFFFF)

没有BMI1,我们需要3条指令,而不是x+1 = 0的1条指令:

# 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

还要检查位组的特定 length ,@ fuz有一个好主意:blsi确保设置了正确的位数(并分别检查它们是连续的)。 Popcnt不是基准,Nehalem之前的CPU没有它,并且如果尝试运行它将出错。

    mov     eax, edi
    neg     eax
    and     eax, edi         # eax = isolate_lowest(x)
    add     eax, edi
    test    eax, edi

答案 1 :(得分:0)

让您的电话号码位于eax中,而所需的前缀长度在edx中。

首先,要获取尾随个数,请计算补数并计算尾随零个数(如果数字不匹配,则分支到fail)。需要cmovz,因为bsf不喜欢以0作为其参数来调用。如果没有出现这种情况,则可以删除前三个说明。

       mov ebx, 32      ; number of bits in an int

       mov ecx, eax
       not ecx
       bsf ecx, ecx     ; count trailing zeroes in ecx
                        ; i.e. trailing ones in eax
       cmovz ecx, ebx   ; give correct result for eax == -1
       cmp ecx, edx
       jnz fail         ; have as many ones as desired?

如果您的CPU具有tzcnt,则可以避免此指令:

        mov ecx, eax
        not ecx
        tzcnt ecx, ecx
        cmp ecx, edx
        jnz fail

请注意,在没有CPU的CPU上,tzcnt被静默解释为bsf,这会破坏您的代码。观察未定义的指令异常不足以确保它存在。

要确保没有设置其他位,请清除后缀并检查结果是否为零:

        lea ecx, [rax+1]
        and ecx, eax      ; ecx = eax & eax + 1
                          ; i.e. clear all trailing 1 bits
        jnz fail          ; fail if any bits are left

尽管最快的实现可能只是创建一个具有所有后缀长度的查询表,然后检查后缀是否匹配:

lut:    dd 00000000h, 00000001h, 00000003h, 00000007h
        dd 0000000fh, 0000001fh, 0000003fh, 0000007fh
        dd 000000ffh, 000001ffh, 000003ffh, 000007ffh
        dd 00000fffh, 00001fffh, 00003fffh, 00007fffh
        dd 0000ffffh, 0001ffffh, 0003ffffh, 0007ffffh
        dd 000fffffh, 001fffffh, 003fffffh, 007fffffh
        dd 00ffffffh, 01ffffffh, 03ffffffh, 07ffffffh
        dd 0fffffffh, 1fffffffh, 3fffffffh, 7fffffffh
        dd ffffffffh

        ...

        cmp lut[edx * 4], eax
        jnz fail