问题描述
最近我一直在处理充满布尔值的大型数组。目前,我使用 .bss
指令将它们存储在 .space
部分,该指令允许我创建字节数组。但是,由于我只需要存储布尔值,因此我希望逐位读取和写入数组中的数据。
目前,我能想到的最好方法是使用 .space
指令和所需存储空间的 1/8,并使用 ((1 << k) & n)
公式获取和设置单个位,其中 { {1}} 是位,k
是数据,但这看起来相当笨重,我想知道是否有更优雅的解决方案?谢谢。 (最好使用 AT&T 语法)
解决方法
对于稀疏位集(全部为真或全部为假,只有少数例外),您可以使用任何集合数据结构(包括哈希表)存储一组索引。您当然可以在 asm 中手动实现任何算法,就像在 C 中一样。可能有一些更专业的数据结构适合各种用途/用例。
对于“普通”布尔数组,您的两个主要选项是
-
每字节解包 1 个 bool,值为 0 / 1,如 C
bool arr[size]
(在.bss
或动态分配中,无论您想放在哪里,与任何字节数组相同)。占用压缩位数组的 8 倍空间(因此缓存占用空间),但非常易于使用。对于随机访问特别有效,尤其是写入,因为您可以存储一个字节而不会打扰其邻居。 (不必读取/修改/写入包含的字节或双字)。
除了缓存占用导致更多缓存未命中,如果它加上您的其余数据不适合任何级别的缓存,较低的密度也不利于搜索、popcounting、复制或设置/清除一系列元素.
如果可以在写入数组的代码中保存指令,则可以允许 0 / 非 0 而不是 0 / 1。但是,如果您想比较两个元素或计算真值或其他任何内容,则在阅读时可能会花费指令。请注意,大多数 C/C++ ABI 对
bool
严格使用 0 / 1 个字节,并将保存bool
的2
传递给 C 函数 could make it crash。 -
每位打包 1 个布尔值,如 C++
std::vector<bool>
。 (除了你当然可以将它存储在任何你想要的地方,不像 std::vector 总是动态分配)。Howard Hinnant 的文章 On
vector<bool>
讨论了位数组擅长的一些事情(具有适当优化的实现),例如搜索true
可以一次检查整个块,例如qword 搜索一次 64 位,AVXvptest
一次 256 位。 (然后tzcnt
或bsf
当您找到非零块时,或多或少与字节元素相同:Efficiently find least significant set bit in a large array?)。所以比字节数组快 8 倍(即使假设缓存命中率相等),除了一些额外的工作,如果使用 SIMD 向量化,在向量中找到正确的字节或双字后找到元素内的位。与仅使用vpslld $7,%ymm0,%ymm0
和vpmovmskb %ymm0,%eax
/bsf %eax,%eax
的字节数组将字节转换为位图并进行搜索。
x86 位数组又名位串指令:mem 操作数慢
x86 确实有位数组指令,如 bt
(位测试)和 bts
(位测试和设置),还有重置(清除)和补码(翻转) ,但它们是 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
这有效地将位索引分为双字索引和双字内位索引。双字索引是通过移位显式计算的(并转换回字节偏移量,以使用缩放索引寻址模式对齐双字)。作为 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 个 uops,而 bts %rsi,(%rdi)
的 10 个 uops 做完全相同的事情(但不需要任何 tmp注册)。
你会注意到我只使用了双字块,不像在 C 中你经常看到使用 unsigned long
或 uint64_t
块的代码所以搜索可以快速进行。但是在 asm 中,对同一内存使用不同大小的访问是零问题,除非您先进行窄存储然后进行宽负载,否则存储转发停顿。更窄的 RMW 操作实际上更好,因为这意味着对不同位的操作可以更紧密地结合在一起,而不会实际创建错误的依赖关系。如果这是一个主要问题,您甚至可以使用字节访问,除了 bts
和朋友只能降低到 16 位 word
操作数大小,因此您必须手动 and $7,%reg
从位索引中提取字节内位。
例如像这样阅读:
# 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,%rsi) if you used BTS,BTR,or BTC
字节加载最好使用 movzx
(又名 AT&T movzbl
)完成,以避免写入部分寄存器。
如果您需要原子地设置位(例如在多线程程序中),您可以lock bts %reg,mem
,或者您可以在寄存器中生成1<<(n&31)
并{ {1}} 如果您不关心旧值是什么。 lock or %reg,mem
无论如何都很慢并且是微编码的,所以如果你需要原子性,你可以直接使用它而不是试图避免疯狂的 CISC 位数组语义。
在多线程情况下,更有理由考虑使用每字节 1 个布尔值,这样您就可以只使用普通的 lock bts
(保证原子性并且不会干扰其邻居:Can modern x86 hardware not store a single byte to memory? ),而不是原子 RMW。