上周,我和一位学生聊天,解释了什么是 SIMD 指令。我强调的是,实际上,所有现代处理器都支持 SIMD 指令或类似指令。诚然,一些小型嵌入式处理器没有,但它们也缺少许多其他标准功能。SIMD 代表单指令多数据,这是一种并行计算架构,允许一条指令同时处理多个数据元素。例如,您可以使用一条指令将 16 个字节与其他 16 个字节进行比较。
假设您有以下字符串: stuvwxyzabcdefgh 。您想知道该字符串是否包含字符“ e ”。您可以使用 SIMD 指令将输入字符串加载到寄存器中,然后(使用一条指令)将其与字符串eeeeeeeeeeeeeeee进行比较。结果将相当于0000000000001000,表示输入中确实包含字母e 。
我们的编程语言倾向于将这些 SIMD 指令抽象化,因此,在软件行业长期工作的人完全有可能根本不知道 SIMD 是什么。事实上,我怀疑大多数程序员都不知道 SIMD 指令。如果你用 JavaScript 编写 Web 应用程序,它不太可能出现在你的讨论话题中。(有趣的是, JavaScript SIMD API曾试图在 JavaScript 中引入 SIMD。)
然而,如果 SIMD 无处不在,但很少有人知道它,那么它还有存在的必要吗?
假设你正在寻找字符串中给定字符的第一个实例。在 C 或 C++ 中,你可以实现如下函数:
const char * naive_find ( const char *开始, const char *结束, char字符) { while (开始!=结束) { 如果( *开始==字符) { 返回开始; } ++开始; } 返回结束; }
naive_find函数在由两个指针start和end定义的字符范围内搜索特定字符的第一次出现。它以指向范围开头的指针 ( start )、指向结尾的指针 ( end ) 和要查找的字符 ( character ) 作为输入。该函数使用while循环逐个字符地遍历范围,在每一步检查当前字符 ( *start ) 是否与目标字符匹配。如果找到匹配,函数将返回指向该位置的指针。否则,它增加start以移动到下一个字符。如果在到达end之前未找到匹配的字符,则函数返回end ,表示在指定范围内未找到该字符。我的函数不支持 Unicode,但它仍然相当通用。
这个函数有什么问题?实际实现时,每个字符可能需要大约 6 条 CPU 指令。实际上,你必须比较指针、取消引用指针、比较结果、递增指针位置等等。你或编译器可以稍微优化一下这个数字,但基本结果就是这样。不幸的是,你的处理器可能无法在每个周期内退出超过 6 条指令。事实上,你的处理器甚至可能无法在每个周期内维持 6 条指令。
因此,简单实现起来,在字符串中查找字符的速度会以处理器的速度甚至更低:如果处理器频率为 4 GHz,则字符串的执行速度为 4 GB/s。重要的是,无论字符串很小且能放入 CPU 缓存,还是很大且位于 CPU 缓存之外,这都可能是正确的。
这有问题吗?4 GB/s 不是很快吗?嗯,比磁盘还慢。我老旧的 PlayStation 5 里的磁盘带宽是 5 GB/s。你可以去亚马逊订购一个 15 GB/s 带宽的磁盘。
相反,让我们与我们在simdutf 库中使用 SIMD 指令实现的“find”函数进行比较。您获得的性能取决于处理器及其支持的 SIMD 指令。让我以我的 Apple M4 处理器作为参考。它对 SIMD 的支持相对较弱,只有 16 字节 SIMD 寄存器。与最近完全支持 64 字节 SIMD 寄存器的 AMD 处理器 (Zen 5) 相比,它相形见绌。尽管如此,我们可以在每个 16 字节块中使用大约 4 条指令。这意味着每个输入字符的指令数减少了 20 多倍。对于几千字节或更大的字符串,我获得以下速度。
朴素搜索 | 4 GB/秒 |
simdutf::find | 110 GB/秒 |
也就是说,simdutf::find 函数的速度提高了 20 倍以上,因为它大幅减少了所需的指令数量。
鉴于我们当前的 CPU 设计,我相信 SIMD 指令实际上是实现良好性能(即处理数据的速度比从磁盘读取数据的速度更快)的必要条件,尤其是在执行字符搜索等常见任务时。
我的基准测试源代码已开放。您可能还会对simdutf 库感兴趣,它提供了许多快速字符串函数。
原文: https://lemire.me/blog/2025/08/09/why-do-we-even-need-simd-instructions/