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

2024-05-16

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

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


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


对于“正常”的布尔数组,您的两个主要选项是

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

    占用打包位数组空间(以及缓存占用空间)的 8 倍,但是非常容易使用。对于随机访问特别有效,尤其是写入,因为您可以存储一个字节而不会干扰其邻居。 (不必读取/修改/写入包含的字节或双字)。

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

    如果可以在写入数组的代码中保存指令,则可以允许 0 / 非 0,而不是 0 / 1。但是,如果您想比较两个元素或计算真实值或其他任何内容,那么在阅读时可能会花费指令。请注意,大多数 C/C++ ABI 严格使用 0 / 1 字节bool,并通过bool拿着一个2到 C 函数可能会导致崩溃 https://stackoverflow.com/questions/54120862/does-the-c-standard-allow-for-an-uninitialized-bool-to-crash-a-program/54125820#54125820.

  • 每包装 1 布尔bit,如 C++std::vector<bool>。 (当然,您可以将其存储在任何您想要的地方,这与总是动态分配的 std::vector 不同)。

    霍华德·辛南特的文章On vector<bool> https://isocpp.org/blog/2012/11/on-vectorbool讨论了位数组擅长的一些事情(通过适当优化的实现),例如寻找一个true可以一次检查整个块,例如使用 qword 搜索一次 64 位,或者使用 AVX 一次 256 位vptest。 (然后tzcnt or bsf当您找到非零块时,或多或少与字节元素相同:有效地找到大型数组中的最低有效设置位? https://stackoverflow.com/questions/67605508/efficiently-find-least-significant-set-bit-in-a-large-array)。因此,比字节数组快 8 倍(即使假设缓存命中相同),除了使用 SIMD 进行向量化时需要进行一些额外的工作,以便在向量内找到正确的字节或双字后查找元素内的位。与字节数组相比vpslld $7, %ymm0, %ymm0 and vpmovmskb %ymm0, %eax / bsf %eax,%eax将字节转换为位图并搜索它。


x86 位数组又名位串指令:内存操作数缓慢

x86 does有位数组指令,例如bt https://www.felixcloutier.com/x86/bt(位测试)和bts https://www.felixcloutier.com/x86/bts(位测试和设置),还有重置(清除)和补码(翻转),但它们是慢速记忆目的地 https://stackoverflow.com/a/32704227/224132和寄存器位索引;实际上,手动索引正确的字节或双字并加载它,然后使用会更快bts %reg,%reg并存储结果。将 bts 汇编指令与 gcc 编译器一起使用 https://stackoverflow.com/questions/1983303/using-bts-assembly-instruction-with-gcc-compiler/33908467#33908467

# 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, 4)   # and store it back

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

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

这会在 CF 中留下旧位的副本,以防万一您关心。bts reg,reg在 Intel 上是单次,在 AND 上是 2 次,所以与手动操作相比绝对值得mov $1, %reg / shl / or.

在现代 Intel CPU 上这只有 5 uops (https://uops.info/ https://uops.info/ and https://agner.org/optimize/ https://agner.org/optimize/),与 10 uop 相比bts %rsi, (%rdi)它执行完全相同的操作(但不需要任何 tmp 寄存器)。

您会注意到我只使用了双字块,而不是像在 C 中您经常看到的代码使用unsigned long or 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(又名 AT&Tmovzbl)以避免写入部分寄存器。

16位访问宽度是最好的权衡

bts适用于 16 位、32 位和 64 位操作数大小,其中 16 位是最窄的大小,我们仍然可以免费对计数进行掩码以获得字内位。movzx大多数 CPU 上的负载与 32 位负载一样便宜。

BMI2 shrx允许复制和移位,但需要另一个寄存器中的计数。在像埃拉托色尼筛这样需要进行大量位访问的用例中,值得在外循环之外使用一条指令来保存内循环中的指令。 (这与您使用 32 位还是 16 位块正交。)

; NASM syntax
; ahead of a loop
    mov     r13d, 4

; inside a loop
; set the bit at index n (ESI) in bit-array at RDI
    shrx    eax, esi, r13d      ; eax = j >> 4 = index of aligned word containing the bit.  Byte offsets would be worse, store-forwarding stalls as well as misalignment
    movzx   edx, word [rdi+rax*2]
    bts     dx, si              ; BTS reg,reg is fast and does the mod 16 part for free.
    mov     [rdi+rax*2], dx

这是我在 x86-64 Linux 的 NASM 中使用的埃拉托斯特尼筛法(https://godbolt.org/z/vcz969bPa https://godbolt.org/z/vcz969bPa对于包含偶数的基本位数组,https://godbolt.org/z/Ee8j1hz1x https://godbolt.org/z/Ee8j1hz1x对于仅包含奇数的位图,因此需要额外的右移计数。令人惊讶的是,在许多情况下,速度并没有更快,也许只是在缓存大小之间的截止点。两者都没有尝试使用 SIMD 一次设置多个位以获取小素数或更好的局部性)。我最终可能会发布答案我评论了这个代码审查问答 https://codereview.stackexchange.com/questions/277090/sieve-of-eratosthenes-in-x86-assembly

在我的筛子中使用字操作数大小可以显着加快速度,对于一些较小的问题大小 IIRC 来说可能会加快 20%,特别是对于更密集的位图(省略偶数),其中设置每第 3 位将意味着大约 32/ 的大链3 = 10.6 次存储/重新加载每个双字,然后再继续下一个。使用较窄的块可以实现大约两倍的内存级别并行性。 (尽管对于较大的步幅,每次访问都是对不同的双字,但使用筛子时,步幅越小,该步幅的总访问次数就越多。)


线程安全原子

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

在多线程情况下,更有理由考虑每个字节使用 1 个 bool,这样您就可以使用简单的movb $1, (%rdi, %rsi)(保证原子性并且不会干扰其邻居:现代 x86 硬件不能将单个字节存储到内存中吗? https://stackoverflow.com/questions/46721075/can-modern-x86-hardware-not-store-a-single-byte-to-memory),而不是原子 RMW。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

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

  • _mm_max_ss 在 clang 和 gcc 之间有不同的行为

    我正在尝试使用 clang 和 gcc 交叉编译一个项目 但在使用时发现一些奇怪的差异 mm max ss e g m128 a mm set ss std numeric limits
  • 一个地址有多少字节? [复制]

    这个问题在这里已经有答案了 在64位机器上 我们知道一个地址是8个字节 然而 我并不完全清楚一个地址中有多少字节的信息 虚拟内存中的每个字节都有一个地址吗 或者内存中的每 64 位都有一个地址 还是取决于架构 如果这取决于架构 那么我应该如
  • 从 DX:AX 寄存器转移到单个 32 位寄存器

    我在添加 16 位乘法的乘积时遇到问题 我想将一年 例如 2015 年 乘以 365 为此 我 mov dx 0 to clear the register mov ax cx cx holds the year such as 2015
  • ICC 中的 -O3 会扰乱内在函数,使用 -O1 或 -O2 或相应的手动汇编即可

    这是后续这个问题 http stackoverflow com questions 49791664 o2 in icc messes up assembler fine with o1 in icc and all optimizatio
  • 从c调用汇编函数

    我试图从 c 调用汇编函数 但我不断收到错误 text globl integrate type integrate function integrate push ebp mov esp ebp mov 0 edi start loop
  • 68000 汇编语言 - CMPI.B

    What are the contents of the CCR and D3 after the following instructions sequence executes Perform the calculation by ha
  • 64 位 Windows 汇编器

    我想对 64 位 Windows 程序集进行编程 最好使用 NASM 我在 google 上查了一下 但似乎找不到 64 位 Windows 编译器 有些网站提到了ml64 但它似乎不再包含在VC 中 我尝试过 32 位程序集 但显然它在我
  • 如何在 MacOS 上使用 nasm 进行编译

    我正在尝试在汇编器上编译并链接我的第一个程序 我尝试编译以下代码 include stud io inc global main section text main xor eax eax again PRINT Hello PUTCHAR
  • 比“add esp, 4”更小的指令

    又是我 我的程序中有很多 add esp 4 我正在尝试减小它的大小 是否有任何更小的指令可以替代 add esp 4 pop edx 或者您不介意破坏的任何其他整数寄存器 这就是现代编译器实际上所做的 https stackoverflo
  • 嵌入式系统:使用汇编语言时的内存布局

    根据我的理解 嵌入式系统运行机器代码 有多种方法可以生成此代码 一种是用 C 等高级语言编写程序 然后使用编译器获得这样的代码 另一种方法是用汇编语言为该嵌入式系统编写指令 并使用汇编器将其转换为机器代码 现在我们得到了加载到系统并执行的机
  • x86 asm 图形设置的分辨率高于 640x480?

    我刚刚开始使用汇编语言 感觉像学习新东西 并且遇到了一些问题 到目前为止 我一直在浏览的所有教程都没有回答 或者太旧而无法知道 1 我尝试了一些搜索 也许我只是不知道正确的关键字 但我找不到用于更改屏幕分辨率等的图形模式的更新列表 我发现的
  • SMP 上如何处理中断?

    SMP 对称多处理器 多核 机器上如何处理中断 内存管理单元是只有一个还是多个 假设两个线程 A 和 B 运行在不同的内核上 同时 访问页表中不存在的内存页面 在这种情况下 将会出现页面错误 并从内存中引入新页面 将会发生的事件的顺序是什么
  • 为什么 LED 保持亮起而不是闪烁?

    这是使用 pic16f676 中的 TIMER0 中断使 LED 闪烁的 MPASM 代码 端口 A 的引脚 0 RA0 未切换至关闭位置 请帮忙 我是图片组装的新手 我想掌握图片 有没有高手帮我学习一下 我需要以 1 秒的间隔眨眼 代码是
  • movsbl指令的作用是什么? [复制]

    这个问题在这里已经有答案了 我在网上搜索过 但找不到明确的示例来理解该指令的作用 因此 如果有人可以举一个例子 这对我来说将会非常有帮助 用符号从字节扩展到长字移动 在Intel语法中 该指令的助记符是MOVSX 当变量类型为 C 时 C
  • 假布尔值=真?

    我在一本书中找到了这段代码 并在 Netbeans 中执行了它 boolean b false if b true System out println true else System out println false 我只是不明白为什
  • 如何构建gcc multilib工具链?

    我正在尝试在新安装的 ubuntu 14 04 的 AMD64 版本上构建 gcc multilib 工具链 它只有 x86 64 gcc 和 g 安装 没有 multilib 支持 我的配置行是 configure disable che
  • 将“NULL”分配给布尔数据类型是否可以接受?

    将 NULL 分配给布尔数据类型是否可以接受 从理论上来说 是的 但这是一件可怕的事情 NULL是一个空指针常量 它被分配给一个指针以使其指向任何内容 ptr NULL now it points to no object anymore
  • 如何使 gcc 为 -fpatchable-function-entry 发出多字节 NOP?

    gcc确实有能力使用多字节用于对齐循环和函数的 NOP 然而当我尝试 fpatchable function entry option https gcc gnu org onlinedocs gcc Instrumentation Opt
  • 32位进程在64位操作系统上可以访问多少内存?

    在 Windows 上 正常情况下 32 位进程只能访问 2GB RAM 或通过 boot ini 文件中的特殊开关访问 3GB 在 64 位操作系统上运行 32 位进程时 有多少可用内存 是否有任何特殊的开关或设置可以改变这种情况 默认
  • mfence 和 asm 易失性 ("" : : : "内存") 的区别

    据我了解 mfence是硬件内存屏障 而asm volatile memory 是编译器障碍 但是 可以asm volatile memory 用来代替 mfence 我感到困惑的原因是这个链接 http gcc gnu org ml gc

随机推荐