在 x86 程序集中存储大量布尔值的最佳方法是什么?

What is the best way to store a large list of Boolean values in x86 assembly?

最近我一直在处理充满布尔值的大型数组。目前,我使用 .space 指令将它们存储在 .bss 部分,这允许我创建字节数组。但是,因为我只需要存储布尔值,所以我希望从数组中逐位读取和写入数据。

目前,我能想到的最好的方法是让 .space 指令的存储空间减少 1/8,并使用 ((1 << k) & n) 公式来获取和设置各个位,其中 k 是位, n 是数据,但这看起来很笨拙,我想知道是否有更优雅的解决方案?谢谢。 (最好使用 AT&T 语法)

对于稀疏位集(除了少数例外,全真或全假),您可以使用任何集合数据结构(包括散列)存储一组索引 table。您当然可以在 asm 中手动实现任何算法,就像在 C 中一样。可能有一些更专业的数据结构适用于各种目的/用例。


对于“普通”布尔数组,您的两个主要选择是

  • 每个字节解包 1 个 bool,值 0 / 1 像 C bool arr[size]
    (在.bss或动态分配,无论你想把它放在哪里,与任何字节数组相同)。

    占用压缩位数组 space 的 8 倍(因此缓存占用空间),但非常易于使用。对于随机访问特别有效,尤其是写入,因为您可以存储一个字节而不干扰其邻居。 (不必 read/modify/write 包含的字节或双字)。

    除了缓存占用会导致更多的缓存未命中,如果它加上您的其余数据不适合任何级别的缓存,较低的密度也不利于搜索、弹出计数、复制或 setting/clearing一系列元素。

    您可以允许 0 / 非 0 而不是 0 / 1,如果这样可以在写入数组的代码中保存指令。但是,如果您想比较两个元素,或者计算真实值或其他任何内容,那么在阅读时可能会花费指令。请注意,大多数 C/C++ ABI 严格使用 0 / 1 字节用于 bool,并将包含 2bool 传递给 C 函数

  • 打包 1 个 bool,就像 C++ std::vector<bool>。 (除了你当然可以将它存储在任何你想要的地方,不像 std::vector 总是动态分配)。

    Howard Hinnant 的文章 On vector<bool> discusses some of the things a bit-array is good at (with an appropriately optimized implementation), e.g. searching for a true can check a whole chunk at a time, e.g. 64 bits at a time with qword searches, or 256 bits at a time with AVX vptest. (Then tzcnt or bsf when you find a non-zero chunk, more or less the same as with byte elements: )。因此比使用字节数组快 8 倍(即使假设缓存命中率相同),除了使用 SIMD 进行矢量化时需要做一些额外的工作,以便在向量中找到正确的字节或双字后查找元素内的位。与仅 vpslld , %ymm0, %ymm0vpmovmskb %ymm0, %eax / bsf %eax,%eax 的字节数组相比,将字节转换为位图并进行搜索。


x86 位数组又名位串指令:mem 操作数慢

x86 是否 有像 bt (Bit Test) and bts (Bit Test and Set), also Reset (clear) and Complement (flip), but they're and a register bit-index; it's actually faster to manually index the right byte or dword and load it, then use bts %reg,%reg and store the result. Using bts assembly instruction with gcc compiler

这样的位数组指令
# fast version:
# set the bit at index n (RSI) in bit-array at RDI
   mov  %esi, %edx           # save the original low bits of the index
   shr  , %rsi             # dword index = bit-index / 8 / 4
   mov  (%rdi, %rsi, 4), %eax   # load the dword containing the bit
   bts  %edx, %eax           # eax |= 1 << (n&31)   BTS reg,reg masks the bit-index like shifts
   mov  %eax, (%rdi, %rsi)   # and store it back

这有效地将位索引拆分为双字索引和双字内位索引。双字索引是通过移位显式计算的(并通过缩放索引寻址模式返回到对齐双字的字节偏移量)。双字内位索引是隐式计算的,作为 bts %reg,%reg 屏蔽计数的一部分。

(如果您的位数组肯定小于 2^32 位(512 MiB),您可以使用 shr , %esi 节省一个字节的代码大小,丢弃位索引的高 32 位.)

这会在 CF 中留下旧位的副本,以备不时之需。 bts reg,reg 在 Intel 上是 single-uop,在 AMD 上是 2 uops,所以绝对值得手动做 mov , %reg / shl / or.

在现代 Intel CPU (https://uops.info/ and https://agner.org/optimize/) 上只有 5 微指令,而 bts %rsi, (%rdi) 则为 10 微指令,后者做同样的事情(但不需要任何 tmp 寄存器) .

你会注意到我只使用了双字块,不像在 C 中你经常看到代码使用 unsigned longuint64_t 块,这样搜索就可以快速进行。但是在 asm 中,使用对同一内存的不同大小的访问是零问题,如果你先做一个狭窄的存储然后进行一个广泛的加载,则存储转发停顿除外。更窄的 RMW 操作实际上更好,因为这意味着不同位上的操作可以更紧密地结合在一起,而不会实际创建错误的依赖关系。如果这是一个主要问题,你甚至可以使用字节访问,除了 bts 和朋友只能下降到 16 位 word 操作数大小,所以你必须手动 and , %reg从位索引中提取位内字节。

例如像这样阅读:

# byte chunks takes more work:
   mov  %esi, %edx      # save the original low bits
   shr  , %rsi        # byte index = bit-index / 8
   movzbl (%rdi, %rsi), %eax   # load the byte containing the bit
   and  , %edx
   bt   %edx, %eax      # CF = eax & 1 << (n&7)
   # mov  %al, (%rdi, %rsi)   if you used BTS, BTR, or BTC 

字节加载最好用 movzx(又名 AT&T movzbl)来避免写入部分寄存器。


如果你需要原子地设置一点(例如在多线程程序中),你可以lock bts %reg, mem,或者你可以生成1<<(n&31)在寄存器中,如果您不关心旧值是什么,则 lock or %reg, memlock bts 很慢,而且是微编码的,所以如果你需要原子性,你可能只是使用它而不是试图避免疯狂的 CISC 位数组语义。

在多线程的情况下,有更多的理由考虑每个字节使用 1 个 bool,这样你就可以只使用普通的 movb , (%rdi, %rsi)(保证原子性并且不会打扰它的邻居:),而不是原子 RMW。