【问题标题】:Can I use SIMD for speeding up string manipulation?我可以使用 SIMD 来加速字符串操作吗?
【发布时间】:2021-03-06 13:24:21
【问题描述】:

SIMD 指令是否仅用于向量数值计算?或者它是否适合于一类字符串操作任务,例如将数据行写入文本文件,其中行的顺序无关紧要?如果是这样,我应该从哪些 API 或库开始?

【问题讨论】:

  • 简短回答:I/O 比 CPU 慢很多数量级,SIMD 无济于事;在没有 SIMD 帮助的情况下,CPU 的运行速度已经比 I/O 层快得多
  • @ShadowRanger:当您读取磁盘缓存中的热文本文件时,情况并非如此。然后它基本上是带有一些系统调用或页面错误开销的内存带宽。对于一些标量逐个字节的循环,每个字节有几个周期,4GHz CPU 只能达到 2GB/s。这仅比现代消费级 SSD 快约 2 倍,而一些一次字节的代码可能比这慢得多。服务器 I/O 可以更快。当然,内存带宽要高得多。 I/O 通常很慢,但并不总是数量级。现代系统并不总是靠一根稻草呼吸。

标签: c++ c string optimization simd


【解决方案1】:

SIMD 指令在非常低的级别上使用。将数据写入文本文件是一个更高级别,涉及缓冲 I/O 等。

您可以使用 SIMD,例如将字符串从小写转换为大写。将 SIMD 包装到库中将没有实际意义。您自己编写说明。这也意味着它们是特定于处理器的(例如 x86/AMD64 上的 SSE 变体)。

要并行处理多行文本,您可以改用微并行化,例如由 OpenMP 或 TBB 提供。

但是,如果您坚持写入文本文件的示例,我们将进入另一个性能优化领域(I/O 而不是计算)。

【讨论】:

  • 所以在最低级别,我需要使用 GCC 的矢量扩展,例如正确吗?
  • 您可以使用它们或包含特定于架构的标头。
【解决方案2】:

是的!这实际上是在高性能解析库中完成的。一个例子:simdjson- 一种每秒可以解析千兆字节 JSON 的解析器。自述文件中有一个About simdjson 部分,其中有一个链接指向讨论一些实现细节的演讲。

SIMD 指令对数值进行操作,但一旦达到该级别,“文本”就只是数值,例如UTF-8 代码点只是无符号的 8 位整数,具有大量 SIMD 支持。处理位图充满了对多个 8 位无符号整数的并行操作,而且很方便地发生这种情况很常见,以至于 SIMD 指令集涵盖了这些操作,因此其中很多也可用于文本处理。

I/O 比 CPU 慢很多数量级

不是真的。它速度较慢,但​​是当 CPU 必须执行会破坏流式传输性能的任务时,例如分支错误预测、缓存未命中或在死胡同上浪费大量推测执行资源时,CPU 很容易跟不上 I/O .用于快速存储访问或多机通信的现代网卡会使 CPU 的内存端口饱和。他们都是。并保持这种状态。但这是最先进的,目前相当昂贵(绑定的 50 GBit 链接等)。顺序的,一次一个字节的解析器代码比这慢得多。

【讨论】:

  • Nievely 我会说 SIMD 无济于事,因为对于长字符串,内存带宽将成为瓶颈。为什么不是这样?
  • 我明白你的意思 - 将所有内容都视为十六进制、二进制或整数。因此,如果我知道在这些宽寄存器中填充什么,我基本上可以使用 SIMD。
  • @doron 内存带宽类似于每两个时钟周期(数量级)的缓存线,因此非常快的代码可以使其饱和,但即使 simdjson 也无法跟上内存 - 几千兆字节/s 远低于内存的能力。最宽的 SIMD 操作可以让您在每个时钟周期对整个高速缓存行执行一到两个操作,但您仍然需要其中不少操作来执行您的任务。
  • @doron:即使数据必须来自 DRAM,现代台式机/笔记本电脑 CPU 也可以在每个核心时钟周期加载大约 8 字节,无论如何都在 2 倍之内。祝你好运跟上一次字节的标量循环。此外,如果您只是执行了read() 系统调用以让内核将网络缓冲区或页面缓存中的一些数据 memcpy 到您的进程的内存中,那么这些数据在 L3 缓存甚至 L2 中可能仍然很热。 Xeon CPU 甚至可以 DMA 进入 L3 缓存,或类似的东西,因此以内存带宽为目标是一个相当低/没有野心的目标,也是不优化的糟糕借口。
  • 此外,更少的指令来处理相同的数据让 OoO exec 看得更远,并且在硬件预取不会(例如跨页边界)的情况下更早地开始对后面的页面/缓存行进行需求加载。 ) 它还可以对超线程更友好,如果在其上运行任何东西,则可以使 HT 同级内核具有更好的吞吐量。 (如果没有很多线程处于活动状态,也许什么都没有)。此外,如果 SIMD 足够高效,它可能会节省电力:通过管道跟踪指令是成本的很大一部分,而不是执行单元本身。
【解决方案3】:

是的,尤其是对于 ASCII,例如Convert a String In C++ To Upper Case。或检查有效的 UTF-8 (https://lemire.me/blog/2020/10/20/ridiculously-fast-unicode-utf-8-validation/),或检查字符串是否恰好是 UTF-8 的 ASCII 子集。 (如果是这样,你知道你有固定宽度的字符,这对其他事情非常有用。)

正如 Daniel Lemire 所报告的,早期的 UTF-8 验证尝试给出了“每个字符几个 CPU 周期”。但使用 SIMD,他和合作者能够实现每字节约 1 条指令,净速度约为 12GB/s。 (相比之下,Haswell 台式机的 DRAM 带宽约为 25GB/s,或 Skylake 的 DDR4-2133 为 34GB/s)。


当然,大多数 C 库已经有函数的手写 asm 实现,如 strlenstrcpystrcasecmpstrstr 等,如果成功则使用 SIMD(例如在 x86-64 上,pmovmskb 允许在任何/所有 SIMD 比较结果为真或假时进行相对有效的比较/分支。)我在Why does glibc's strlen need to be so complicated to run quickly? 上的回答的第一部分有一些指向 glibc 的手动优化 asm 的链接实际上在主流平台上使用,而不是问题所问的便携式纯 C 后备。

https://github.com/WojciechMula/sse4-strstr 有多种strstr 实现。子字符串搜索是一个更难的问题,具有非平凡的算法选择以及蛮力。 SSE4.2“字符串”指令可能对此有所帮助,但如果没有,那么 SIMD 向量比较肯定可以得到更好的蛮力构建块。

(像pcmpistri 这样的SSE4.2“字符串”指令对于memcmp / strcmpstrlen 来说肯定更糟,其中普通的SSE2(或AVX2)更好。见How much faster are SSE4.2 string instructions than SSE2 for memcmp?https://www.strchr.com/strcmp_and_strlen_using_sse_4.2

您甚至可以通过基于矢量比较位图查找随机播放控制矢量来做一些很酷的技巧,例如Fastest way to get IPv4 address from stringHow to implement atoi using SIMD?。 虽然我不确定 SIMD atoi 是否胜过标量,尤其是对于短数字。


我会天真地说 SIMD 无济于事,因为对于长字符串,内存带宽将成为瓶颈。为什么不是这样?

DRAM 带宽 与现代 CPU 速度相比确实相当不错,尤其是当数据以字节块而不是 8 字节 double 块的形式出现时。并且数据在复制后通常在 L3 缓存中很热(例如来自read 系统调用)。

即使数据必须来自 DRAM,现代台式机/笔记本电脑 CPU 也可以在每个核心时钟周期加载大约 8 个字节,无论如何都在 2 倍之内,尤其是在该核心不与其他带宽密集型代码竞争的情况下在其他核心上。祝你好运,通过一次字节的标量循环跟上这一点。

此外,如果您只是执行了read() 系统调用以让内核将网络缓冲区或页面缓存中的一些数据 memcpy 到您的进程内存中,那么这些数据在 L3 缓存甚至 L2 中可能仍然很热。 Xeon CPU 甚至可以 DMA 进入 L3 缓存,或类似的东西。以内存带宽为目标是一个相当低/没有野心的目标,如果一个函数实际上被大量使用 很多,它也是一个不充分优化函数的糟糕借口。

处理相同数据的指令越少,乱序 exec 就可以“看到”更远的地方,并在硬件预取无法实现的情况下更早地开始对后面的页面/缓存行进行按需加载(例如跨越页面边界)。并且更好地将字符串处理与早期/后期的独立工作重叠。

它还可以对超线程更友好,如果在其上运行任何东西,则可以使 HT 同级内核具有更好的吞吐量。 (如果没有很多活动线程,也许什么都没有)。此外,如果 SIMD 足够高效,它可能会节省能源:通过管道跟踪指令是成本的很大一部分,而不是整数执行单元本身。跑步时功率更高,但更快完成,是好的:赛跑睡觉。 CPU 在完全空闲时比仅运行“廉价”指令时节省更多的电量。

【讨论】:

  • DRAM 带宽对于具有 2 个或可能 4 个内核的客户端处理器来说并不是灾难性的糟糕,但它在服务器空间中就不够用了,正如我在过去 29 年中所记录的那样:sites.utexas.edu/jdm4372/2016/11/22/…跨度>
猜你喜欢
  • 1970-01-01
  • 2013-03-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-10-04
  • 2018-06-17
  • 2018-01-27
相关资源
最近更新 更多