assembly - 在 x86 程序集中存储大量布尔值的最佳方法是什么?
问题描述
最近我一直在处理充满布尔值的大型数组。目前,我将它们存储在.bss
带有.space
指令的部分中,该指令允许我创建字节数组。但是,由于我只需要存储布尔值,我希望从数组中逐位读取和写入数据。
目前,我能想到的最好方法是.space
使用所需存储空间的 1/8 的指令,并使用((1 << k) & n)
公式获取和设置各个位,位在哪里k
,n
数据在哪里,但这似乎相当笨重,我想知道是否有更优雅的解决方案?谢谢。(最好使用 AT&T 语法)
解决方案
对于稀疏位集(除了少数例外,全部为真或全部为假),您可以使用任何集合数据结构(包括哈希表)存储一组索引。您当然可以在 asm 中手动实现任何算法,就像在 C 中一样。可能有一些更专业的数据结构适用于各种目的/用例。
对于“正常”的布尔数组,您的两个主要选项是
每个字节解压 1 个布尔值,值 0 / 1 像 C 一样
bool arr[size]
(在.bss
或动态分配的,无论你想放在哪里,与任何字节数组相同)。占用压缩位数组的 8 倍空间(因此缓存占用空间),但非常易于使用。对于随机访问特别有效,尤其是写入,因为您可以存储一个字节而不会干扰其邻居。(不必读取/修改/写入包含的字节或双字)。
如果缓存占用空间加上其余数据不适合任何级别的缓存,则会导致更多缓存未命中,此外,较低的密度也不利于搜索、弹出计数、复制或设置/清除一系列元素。
如果可以在写入数组的代码中保存指令,则可以允许 0 / 非 0 而不是 0 / 1。但是,如果你想比较两个元素,或者计算真实值或其他什么,这可能会在阅读时花费说明。请注意,大多数 C/C++ ABI 严格使用 0 / 1 字节
bool
,并且将 abool
保存 a传递2
给 C 函数可能会使其崩溃。每比特打包 1 个 bool ,如 C++
std::vector<bool>
。(除了你当然可以将它存储在任何你想要的地方,不像 std::vector 总是动态分配)。Howard Hinnant 的文章On
vector<bool>
讨论了位数组擅长的一些事情(通过适当优化的实现),例如搜索 atrue
可以一次检查整个块,例如使用 qword 搜索一次 64 位,或 256 位一次与 AVXvptest
。(然后tzcnt
或bsf
当您找到一个非零块时,或多或少与字节元素相同:有效地找到大数组中的最低有效位?)。因此,比使用字节数组快 8 倍(即使假设缓存命中相同),除了使用 SIMD 进行向量化时的一些额外工作,以在向量中找到正确的字节或 dword 后找到元素中的位。与字节数组相比,只是vpslld $7, %ymm0, %ymm0
和vpmovmskb %ymm0, %eax
/bsf %eax,%eax
将字节转换为位图并进行搜索。
x86位数组又名位串指令:mem操作数慢
x86确实有位数组指令,例如bt
(Bit Test) 和bts
(Bit Test and Set),还有 Reset (clear) 和 Complement (flip),但是它们在内存目标和寄存器位索引方面很慢;实际上,手动索引正确的字节或双字并加载它,然后使用bts %reg,%reg
并存储结果会更快。 使用 bts 汇编指令和 gcc 编译器
# 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
。
这在现代英特尔 CPU(https://uops.info/和https://agner.org/optimize/)上只有 5 微指令,而 10 微指令的bts %rsi, (%rdi)
作用完全相同(但不需要任何 tmp 寄存器)。
你会注意到我只使用了 dword 块,不像在 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, (%rdi, %rsi) if you used BTS, BTR, or BTC
字节加载最好使用movzx
(aka AT&T movzbl
)来避免写入部分寄存器。
如果您需要自动设置一些位(例如在多线程程序中),您可以lock bts %reg, mem
,或者您可以1<<(n&31)
在寄存器中生成,lock or %reg, mem
如果您不关心旧值是什么。 lock bts
无论如何都是缓慢和微编码的,所以如果你需要原子性,你可以只使用它而不是试图避免疯狂的 CISC 位数组语义。
在多线程的情况下,有更多理由考虑每个字节使用 1 个 bool,这样您就可以使用普通movb $1, (%rdi, %rsi)
的(保证原子并且不会干扰其邻居:现代 x86 硬件可以不将单个字节存储到内存吗?) ,而不是原子 RMW。
推荐阅读
- linux - 以下 unix 代码的输出是什么?
- laravel - SQLSTATE [22007]:无效的日期时间格式:1366 不正确的整数值:列“状态”的“未定义”
- cassandra - Cassandra 令牌环的工作原理
- apache - Apache HTTP - 从包含指令中排除特定文件
- java - 这里的设置器或 if 语句有什么问题
- c# - 从 Web 应用程序(asp.net 核心)中的视频/音频文件获取持续时间
- spring - 无法从 Spring Boot 应用程序中的浏览器访问 http:inbound-gateway 映射
- php - Laravel/Blade 布局文件中不同内容的切换
- javascript - 如何从数据库 ondelete 重新渲染列表视图
- jsf - p:menuitem 的 url 和结果属性有什么区别?