对于稀疏位集(全部为真或全部为假,少数例外),您可以使用任何集合数据结构(包括哈希表)存储一组索引。您当然可以在 asm 中手动实现任何算法,就像在 C 中一样。可能有一些更专业的数据结构适用于各种目的/用例。
对于“正常”的布尔数组,您的两个主要选项是
-
每个字节解压 1 个 bool,值为 0 / 1,如 C bool arr[size]
(在.bss 或动态分配,无论你想放在哪里,与任何字节数组相同)。
占用的空间是打包位数组的 8 倍(从而占用缓存空间),但非常易于使用。对于随机访问特别有效,尤其是写入,因为您可以存储一个字节而不会干扰其邻居。 (不必读取/修改/写入包含的字节或双字)。
除了缓存占用空间会导致更多缓存未命中,如果它加上其余数据不适合任何级别的缓存,较低的密度也不利于搜索、弹出计数、复制或设置/清除一系列元素.
如果可以在写入数组的代码中保存指令,则可以允许 0 / 非 0,而不是 0 / 1。但是,如果你想比较两个元素,或者计算真实值或其他什么,这可能会在阅读时花费说明。请注意,大多数 C/C++ ABI 对 bool 严格使用 0 / 1 字节,并将持有 2 的 bool 传递给 C 函数 could make it crash。
-
每个 bit 打包 1 个 bool,例如 C++ std::vector<bool>。 (除了你当然可以将它存储在任何你想要的地方,不像 std::vector 总是动态分配)。
Howard Hinnant 的文章 On vector<bool> 讨论了位数组擅长的一些事情(通过适当优化的实现),例如搜索 true 可以一次检查整个块,例如使用 qword 搜索一次 64 位,或者使用 AVX vptest 一次 256 位。 (然后tzcnt 或bsf 当你找到一个非零块时,或多或少与字节元素相同:Efficiently find least significant set bit in a large array?)。所以比使用字节数组快 8 倍(即使假设缓存命中相同),除了一些额外的工作,如果使用 SIMD 向量化,在向量中找到正确的字节或 dword 后找到元素中的位。与字节数组相比,只需 vpslld $7, %ymm0, %ymm0 和 vpmovmskb %ymm0, %eax / bsf %eax,%eax 将字节转换为位图并进行搜索。
x86 位数组又名位串指令:mem 操作数很慢
x86 确实具有位数组指令,例如bt(位测试)和bts(位测试和设置),还有重置(清除)和补码(翻转),但它们' re slow with a memory destination 和一个寄存器位索引;手动索引正确的字节或双字并加载它实际上更快,然后使用bts %reg,%reg 并存储结果。 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 $5, %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
这有效地将位索引拆分为双字索引和双字内位索引。 dword 索引是通过移位显式计算的(并使用缩放索引寻址模式转换回对齐 dword 的字节偏移量)。 bit-within-dword 索引是隐式计算的,作为bts %reg,%reg 掩码计数的一部分。
(如果您的位数组肯定小于 2^32 位(512 MiB),您可以使用shr $5, %esi 节省一个字节的代码大小,丢弃位索引的高 32 位。)
这会在 CF 中留下旧位的副本,以备不时之需。 bts reg,reg 在 Intel 上是单 uop,在 AMD 上是 2 uop,因此绝对值得与手动执行 mov $1, %reg / shl / or。
这在现代 Intel CPU(https://uops.info/ 和 https://agner.org/optimize/)上只有 5 uop,而 bts %rsi, (%rdi) 的 10 uop 则完全相同(但不需要任何 tmp 寄存器)。
你会注意到我只使用了 dword 块,不像在 C 中你经常看到使用 unsigned long 或 uint64_t 块的代码,因此搜索可以快速进行。但是在 asm 中,使用不同大小的访问相同的内存是零问题,除了存储转发停顿,如果你先做一个窄存储然后再做一个宽负载。更窄的 RMW 操作实际上更好,因为它意味着不同位上的操作可以更接近,而不会实际创建错误的依赖关系。如果这是一个主要问题,您甚至可以使用字节访问,除了 bts 和朋友只下降到 16 位 word 操作数大小,因此您必须手动 and $7, %reg 来提取位内-byte 来自位索引。
例如喜欢阅读:
# byte chunks takes more work:
mov %esi, %edx # save the original low bits
shr $3, %rsi # byte index = bit-index / 8
movzbl (%rdi, %rsi), %eax # load the byte containing the bit
and $7, %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, mem不在乎旧值是什么。 lock bts 很慢而且微编码,所以如果你需要原子性,你可以使用它而不是试图避免疯狂的 CISC 位数组语义。
在多线程的情况下,有更多理由考虑每字节使用 1 个布尔值,这样您就可以只使用普通的 movb $1, (%rdi, %rsi)(保证原子且不会干扰其邻居:Can modern x86 hardware not store a single byte to memory?),而不是一个原子 RMW。