【问题标题】:Test for all true bits being adjacent测试所有相邻的真实位
【发布时间】:2019-03-24 18:32:00
【问题描述】:

我正在通过遍历树来构建 Huffman 前缀代码 LUT。我正在使用寄存器来跟踪当前前缀。

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

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

  2. 当前前缀码的所有位都设置为没有假位。

对于第二个条件,我也在跟踪前缀字符串的当前长度(以位为单位)。

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

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

【问题讨论】:

  • 你能假设 BMI1(和 BMI2)吗?我的第一个想法是,将 1 添加到连续组的低位将清除所有带有进位的位。 (0xff + 1 = 0x100),因此您想添加最低设置位并查看结果是否恰好设置了 1 位(即测试它是否是 x & (x-1) == 0graphics.stanford.edu/~seander/… 的 2 的幂)。 BMI1 blsi = 隔离最低设置位。
  • @PeterCordes 我喜欢这个想法,我在前缀上使用inc 来实现它,然后使用bt prefix, prefix length,如果它都是连续的设置位,则以 CF 设置结束。谢谢!编辑:啊,我明白为什么我现在需要二的力量了..
  • 此方法不适用于前缀以 0(高位零)开头的情况。我在不同的寄存器中有长度,长度为 3,前缀代码 001,它在检查中评估成功,它应该失败。
  • 哦,你的连续位保证在寄存器的底部?这比标题和最终问题的一般情况更容易!为您保存 blsi(-x) & x 以隔离最低组。并且 + 知道当前前缀长度可以启用 bt,是的,这非常好,比我想出的区分 1 位集与多位集要容易得多。 (例如 godbolt.org/z/LaOW27 blsi / add / blsr 如果输入只有 1 或 0 个连续位组,则设置 ZF,但不排除它只有 1 位长。)
  • @PeterCordes 是的,设置位必须在一起并从位 0 开始。我不明白“blsi”指令。所以我需要测试一组分组位,bit0 和 bit(prefixlength-1)?

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


【解决方案1】:

构建块将是:将 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 &amp; (x+1) 在寄存器底部检查单个长度未知的连续位组,它会检测是否设置了任何更高位。如果存在未通过进位传播翻转的高位,则 AND 或 TEST 将产生非零结果。

但这会将01 视为像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) &amp; 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:

【讨论】:

    【解决方案2】:

    让您的号码为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 上,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
    

    【讨论】:

    • 我已经对问题添加了编辑,我没有解释得很好。
    • @我会修改我的答案来解决这个问题。
    • @PeterCordes 一个更简单的方法是设置一个包含所需后缀的寄存器(即通过 LUT 或变量移位然后递减)并简单地进行比较。
    • 哦,好点,是的xor-零注册,然后bts edx, len/dec edx创建(1&lt;&lt;len) - 1。 BTS 是一个经常被忽视的有效创建1&lt;&lt;n 的指令,尤其是在 Intel CPU 上,它是单 uop 与 AMD 上的 2。
    • 我想出了一个技巧,它使用 +1btr / test 在候选条件上仅在 3 微秒内分支,无需 LUT 或预计算。发布了我一直在研究的答案。
    猜你喜欢
    • 1970-01-01
    • 2017-02-16
    • 1970-01-01
    • 1970-01-01
    • 2020-01-25
    • 1970-01-01
    • 1970-01-01
    • 2010-11-17
    • 1970-01-01
    相关资源
    最近更新 更多