2026年Rust中SIMD的现状
Fearless SIMD 维护者撰写的年度 SIMD 库调研,对比 std::simd、wide、pulp、macerator 并详解 x86 指令集兼容难题。
中文
复制
去年以来进步很大,其中也有我的一份。看完去年的调研之后,我开始给当时看起来最有前途的那个 SIMD 库提交代码。一来二去,我现在成了 Fearless SIMD 的维护者。为了避免利益冲突,我邀请其他几个库(std::simd、wide、pulp、macerator)的作者审阅了本文草稿并提出意见。不过编辑权仍在我手里,所有错误也都由我负责。今年的调研比上一版更深入。系好安全带,我们从头开始。
什么是 SIMD?为什么要用 SIMD?
做算术的硬件很便宜,所以这个世纪出厂的 CPU 都不缺算力。但指令译码单元只有一个,而且很难做快,于是算术硬件大部分时间都闲着。要绕开译码这个瓶颈,可以一次性把一批数字喂给 CPU,让它们执行同一条算术指令,比如加法。这就是 SIMD 这个名字的由来:“single instruction, multiple data”,单指令多数据。你不再是两个数相加,而是两批数——也就是两个“向量”——相加,耗时和只做一次加法差不多。在近几年的 x86 芯片上,这批数据最大可以到 512 位,所以理论上 f64 上的数学运算能快 8 倍,u8 上能快 64 倍。实际跑起来,可能更快,也可能更慢。
指令集
历史上,SIMD 指令是在 CPU 架构设计完成之后才加进去的,所以在每个架构上它都是一个扩展,各自带着自己的营销名字。ARM 的叫 “NEON”,所有 64 位 ARM CPU 都支持。WebAssembly 没有市场部,就直接叫 “WebAssembly 128-bit packed SIMD extension”。64 位 x86 出厂时带的是 “SSE2”,提供 128 位向量的基础指令,但后来他们在这之上又加了一大堆扩展:SSE 4.2 增加了更多运算,AVX 和 AVX2 带来 256 位向量,AVX-512 则带来 512 位向量和更多运算。上面这段话里的“后来”二字,制造了一个问题。
这颗 CPU 支持那条指令吗?
在 x86_64 CPU 上跑程序时,CPU 是否支持某个特定的 SIMD 扩展并不是理所当然的。所以默认情况下,编译器不允许使用 SSE2 以外的指令,因为那无法在所有 x86_64 CPU 上运行。绕开这个问题有两种办法。如果你所在的公司只在自家服务器或公有云上运行二进制文件,那你可以直接认定它们都足够新,至少支持十多年前就引入的 AVX2,一旦跑在不支持 AVX2 的机器上就让程序崩溃或行为异常:
RUSTFLAGS='-C target-cpu=x86-64-v3' cargo build --release
但如果你要把二进制分发给别人运行,这就行不通了。这时可以用函数多版本化(function multiversioning):把同一个函数针对不同的 SIMD 扩展分别编译多份,程序运行时检查 CPU 支持哪些特性,据此选择对应的版本。好在只有 x86 有这个问题。ARM 在 64 位 CPU 上强制要求 NEON,此后也没再添加什么有用的 SIMD 扩展(后面细说)。WebAssembly 则要求你编译两个不同的二进制文件,一个带 SIMD、一个不带,再用 JavaScript 检查浏览器是否支持 SIMD。
怎么用 SIMD?
利用 SIMD 有三种方式:
#[cfg(all(any(target_arch = "x86", target_arch = "x86_64"),target_feature = "sse2"))]
_mm_add_epi32(__m128i , __m128i)
#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
vaddq_u32(int32x4_t, int32x4_t)
下面看看每种方式具体要做什么,以及各自的编程模型现状如何。
自动向量化
直接写普通的 Rust,让编译器的启发式规则去干活!只要注意把代码写成编译器能够(大致)可靠向量化的形式,效果可以相当不错。通常这意味着遍历 &[i32].as_chunks() 而不是 &[i32],再通过基准测试或盯着汇编来确认它确实生效了。细节见 Can You Trust a Compiler to Optimize Your Code?。这是最省事的方式,不依赖任何库,而且自动支持编译器支持的所有指令集,无论多冷门。缺点是不够可靠。函数越大越复杂,编译器无法向量化的可能性就越高。性能还可能因编译器版本不同或周边代码的改动而剧烈波动。浮点类型也需要特别小心。
浮点数很怪。 哪怕只是以尚可的精度对一个浮点数组求和,事情也会变得出奇复杂,参见 Taming Floating-Point Sums。
过去,自动向量化对浮点类型不适用,因为它会改变结果的精度(通常还是往好的方向变,但编译器无权改动任何可观测结果)。Rust 1.98 改变了这一点,稳定了诸如 algebraic_add() 这类代数运算,允许编译器改动可观测结果,相当于一个没那么危险的 -ffast-math。不过在多数情况下,你仍然得改写代码去用它们,才有资格被向量化。而且你还是得想办法搞到 multiversioning。既然如此,那就顺便说说……
multiversion crate
下面讨论的那些一体化 SIMD crate 也提供 multiversioning,但我们先快速看一下 multiversion,因为它对自动向量化最有用。用法非常简单:给函数加上 #[multiversion(targets = "simd")] 注解就完事了。但这种简单掩盖了一个没有文档记载的坑:调用带 #[multiversion] 注解的函数会有一点额外开销。开销非常小——不到十几条指令,但如果你加注解的函数本身就很短小,这点开销就会显得相当可观。经验法则是:如果函数里有循环,就加 #[multiversion];如果它只处理少量值,就加 #[inline(always)],前提是调用链上游某处有 #[multiversion]。下面列出的其他 crate 没有这个坑,也不用你去考虑函数的大小,代价是更多样板代码。multiversion 是唯一允许你列出所需具体 CPU 扩展的 crate,而不是选择某个预定义的 SIMD 等级。所以如果你的代码恰好能从某个很新的指令中获益,你可以选择启用它。但以我的经验,对自动向量化的代码来说这种情况几乎不会出现。对于 AVX-512,multiversion 检查的是它是否存在,而不是它是否真的快,这在实践中可能损害性能(详见下文)。你可以用样板代码绕过这一点——你得给每个函数都加上这段:
#[multiversion::multiversion(targets(
"x86_64+cmpxchg16b+popcnt+sse3+sse4.1+sse4.2+ssse3", // x86_64-v2
"x86_64+avx+avx2+bmi1+bmi2+cmpxchg16b+f16c+fma+lzcnt+movbe+popcnt+sse3+sse4.1+sse4.2+ssse3+xsave", // x86_64-v3
"x86_64+fxsr,adx,avx512bitalg,avx512bw,avx512cd,avx512dq,avx512f,avx512ifma,avx512vbmi,avx512vbmi2,avx512vl,avx512vnni,avx512vpopcntdq,bmi1,bmi2,cmpxchg16b,fma,gfni,lzcnt,movbe,pclmulqdq,popcnt,vpclmulqdq,xsave,xsavec,xsaveopt,xsaves", // Ice Lake and later
)]]
可移植 SIMD 抽象
目前已有若干可用于生产环境的方案。理想特性如下:
速览表:
以及指令集支持情况:
- macerator 还支持 LoongArch,因为作者的原话是“闲得慌”。
std::simd
std::simd 并不是 SIMD 的完整解决方案。它更像是一组必须放进标准库的积木,其余的一切都交给生态里的 crate。最大的缺点是它只能在 nightly 上使用,而且 API 仍会不定期地发生破坏性变更。所以某天你更新了编译器,代码就编译不过了,还得回头去修。但只要你能接受这一点,并且只需要定宽向量、顶多再加个 multiversioning,那它相当不错!std::simd 存在的意义就是直接架在 LLVM 之上,能瞄准 LLVM 支持的任何平台,包括只有大型银行在用、或者只有中国政府在用的那些奇怪 CPU。反过来说,如果 LLVM 内部没有一个完全对得上的操作供 std::simd 使用,那就没有备选方案,实际上根本不会用到 SIMD。这种情况出现得频繁得令人不安。它的 sin() 再贴切不过:披着 SIMD 外衣发布标量实现,是头号大罪。而 reduce_sum() 不知怎么的,在性能和精度上都是最差的情况。所以浮点数上的任何非平凡函数都别费劲去用了。我们最接近正经三角函数的,是 sleef crate——SLEEF 向 std::simd 的部分移植,只是稍微有点 bug。而这已经是整篇文章里我能拿到的最好的三角函数了!std::simd 在 multiversioning 方面格外灵活。你可以用 multiversion crate,也可以用本节中任何其他 SIMD crate 提供的 multiversioning。其他所有 crate 都只能用各自内置的 multiversioning。它的 Simd API 看上去非常优雅,只要你能直接对 N 做数学运算就会很好用,但你做不到。这个特性即使在 nightly 上也很不完整。没有它,用 Simd 拿到硬件尺寸的向量也不是不行,只是难看得多。虽然很多情况下你可以直接用 std::simd,性能也还过得去,但靠第三方 crate 东拼西凑地加特性,终究走不远。举个例子,sleef crate 不能和 multiversion crate 一起用,你得 fork sleef 自己把它们配到一起。第三方扩展各自为政,组合不起来。你需要的是一个所有部件都能协同工作的全能方案。说到这个……
fearless_simd
Fearless SIMD 是一套一体化方案,所有部件协同工作。看看表格里那一整列漂亮的绿色勾选框就知道了!除了表格里的内容,fearless_simd 独有的特性还有:
主要缺点是样板代码:你没法写
fn my_func(a: A, b: B) {
而必须写
#[simd]
fn my_func(simd: S, a: A, b: B) {
实在啰嗦。AVX-512 只在较新的 CPU 上使用,在那里它不会拖累性能(见下文硬件部分)。你可以手动配置 multiversion 让它表现得像这样,但这不是默认行为,而且需要大量样板代码(见上文)。其他所有 SIMD 抽象 crate 只是检查 AVX-512 是否存在而已。它最近发布了 v1.0,还附带了安全策略等等。最大的缺口是三角函数。目前还没有把 SLEEF 之类的东西移植到 fearless_simd 机制上的实现。
wide
wide 优点很多:平台覆盖好,实现的操作多,而且已经是 v1.0。它甚至有三角函数,尽管其精度被明确标注为未指定。最大的缺点是它与 multiversioning 从根本上不兼容。如果你不针对 x86,或者总是用 -C target-cpu= 为已知硬件构建,那倒无所谓,但否则性能会大打折扣。唯一的变通办法是 cargo multivers,但它只适用于长时间运行的程序,否则其启动开销会远超 SIMD 带来的性能收益。另一个缺点是不支持任何形式的泛型,无论是针对元素类型还是向量宽度。不过你可以用宏绕过这一点。与其把函数写成泛型,不如把它包进 macro_rules!,然后写 $type::from_slice 而不是 T::from_slice。这会增加一点样板代码,但省去了泛型约束的样板代码,有得有失。我这么干过,不算太糟,尤其是引入 paste crate 之类的工具之后。如果你只针对少数几种类型,比如 f32 和 f64,那即便是在有泛型的库里,你可能也还是想用宏方案,因为它还能让你拥有“泛型”大小的数组。真正的泛型数组大小是 nightly-only 且不完整的功能。但你也可以用 generic-array crate 绕过,或者干脆创建一个最大可能 SIMD 大小的数组。
pulp
pulp 是为支撑 faer 线性代数库而构建的。这也决定了它的优先级:实现的操作大多是数学运算(比如没有 swizzle),API 也围绕原生宽度的向量设计。定宽向量在技术上可行,但完全没有文档,用起来也相当别扭。我在研究它们的时候顺手提交了一些修复,其中包括一个健全性 bug。它不原生支持对元素类型做泛型,不过我在 wide 里描述的那套宏技巧在这里同样适用。它的多版本化是我见过最啰嗦的。有个宏可以减少样板代码,但即便如此,跟其他方案比起来还是相当啰嗦。
macerator
macerator 是 pulp 的近亲,设计思路相似。它是为支撑 burn 的 CPU 后端而构建的。跟 pulp 相比,它增加了对元素类型泛型代码的支持,但去掉了对 intrinsic 的安全访问和大部分文档。定宽向量完全没有尝试。它还默认启用 SSE4.2,并为 LoongArch 增加了优化过的代码路径。除了 std::simd,这是唯一一个对 f16 数据提供部分可移植操作的库,尽管支持的操作列表非常有限。在 AVX-512 上使用它需要 nightly 编译器,而 NEON 在 stable 上就能用。用起来还是相当别扭,因为标准库的 f16 只在 nightly 上可用,而这个 crate 只能在没有它的情况下凑合。在 crates.io 上,除了 burn 没有任何东西在用它。
其他
我为单一特定项目做的 SIMD crate(比如 jxl_simd、pathfinder_simd)不在讨论范围内,因为它们并不面向普通用户。主要由 AI 驱动开发的 crate(比如 magetypes、simdeez、thermite)也不包括在内,因为我无法推荐它们用于生产环境,尤其是后两个的 bug 多得让人不安。
安全访问 intrinsic
可移植 SIMD 很好,但有时你想要一条只有特定指令集才有的具体指令。这种情况下就只能直接用 intrinsic。Rust v1.87+ 允许安全地调用平台特定的 intrinsic:
#[target_feature(enable = "avx2")]
fn add_avx2(a: __m256, b: __m256) -> __m256 {
_mm256_add_ps(a, b) // this is an avx2 intrinsic
}
有两个地方需要注意:
但这两个问题都已有成熟的解决方案:
这份清单上的所有东西,都是这两个思路的不同实现。
archmage
archmage 提供 CPU 特性令牌,并用 safe_unaligned_simd crate 做安全的 load/store 封装。它的核心是 #[arcane] 过程宏。你可以从一组预定义的 SIMD 级别中挑选:SSE2/SSE4.2/AVX2 这些老面孔,以及两档不同的 AVX-512——早期实现很慢,Ice Lake 及之后才真正可用,随你选。ARM 这边有基线 NEON 加上几个扩展级别。不支持 32 位 x86(这种情况永远走标量回退),不过 2026 年了,这也不算什么事。
fearless_simd
fearless_simd 通过 kernel! 宏提供的基本上是和 archmage 一样的工具。这是声明式宏,不是过程宏。构建时间因此更短,但不像 archmage 那样支持给泛型或 const 泛型函数加标注。反正 intrinsics 和泛型本来就合不来,所以通常也无所谓。它没有捆绑 safe_unaligned_simd,因为安全加载可以通过它的可移植 SIMD 抽象完成;但如果你非要把加载写成 _mm256_loadu_epi64(),也可以自己引入,通常是为了移植已经那么写的代码。SIMD 级别和可移植 SIMD 抽象一致。所以就算你真的很懂行,也没法主动选择早期那档慢速 AVX-512,也访问不了 NEON 的非基线扩展,比如 aes 或 bf16。好处是可以轻松混用可移植 SIMD 和 intrinsics。这样算法的大部分可以用可移植 SIMD 写,只在真正需要的地方用少量 intrinsics。
pulp
pulp 在这方面强得有点出人意料。如果你想用不属于任何 SIMD 级别的 intrinsics,比如 aes 特性里的 _mm_aesenc_si128,这是安全使用它们的最佳(也是唯一)方式,不用自己造 SIMD 特性令牌。可惜它没有文档说明该怎么做。翻文档你会看到一堆以各种 CPU 特性命名的结构体,比如 Avx512ifma,有构造方法,上面挂着对应这个 CPU 特性的 intrinsics。于是你会以为,构造出来直接调函数就行,对吧?错。这么做确实能跑,但性能惨不忍睹。你是在从一个不保证具备所需 CPU 特性的函数里调用需要额外 CPU 特性的 intrinsic(别忘了,CPU 特性检查可能失败),所以这个 intrinsic 必须单独放在一个函数里。于是你为了执行一条指令,付出了函数调用的开销——好几条指令。“好几条指令”可比一条多得多,函数调用开销占了大头,性能自然一落千丈。正确的做法是创建一个启用了所有所需特性的 context,然后从这个 context 里调用一堆 intrinsics。像这样:
pulp::simd_type! {
pub struct Ifma {
pub ifma: "avx512ifma",
}
}
if let Some(isa) = Ifma::try_new() {
isa.vectorize(
#[inline(always)]
|| {
// Put the entire hot loop here.
// isa.ifma._mm512_madd52lo_epu64(...)
},
);
}
想找一个能真正跑起来的更完整示例,看这里。
为求完整,我还得提一句:fearless_simd 仓库里也曾提出过一个类似的功能,做成一个独立、单独的 crate。它已经完整实现了,但没人愿意站出来实际维护它,所以一直没合进去。如果你对 pulp 有什么不满意的地方,不妨试试那个,看看自己愿不愿意把它接过来。
intrinsics 的现状
除了std::simd和自动向量化,所有 SIMD 代码都建立在 SIMD intrinsics 之上。它们本身也相当直白:intrinsics 理应清晰地对应到特定的 CPU 指令。既然这么简单又这么重要,你自然会以为它们工作得很好。并没有。Rust 里没有,C++ 里没有,C 里也没有。「给我这条确切的指令」和编译器优化之间存在根本性的矛盾。如果你让编译器把 SIMD intrinsics 当作纯粹的黑盒,其他地方就会付出效率代价。比如我在 ARM 上遇到的一个真实 bug:u32x4::from([1,2,3,4])很慢。这实际上就是加载一个常量,而u32x4的内存布局与四个u32组成的数组完全相同,所以它本应非常廉价——只需一次加载。结果发现,底层的 ARM 加载 intrinsic vld1_u32_x4在编译器里被实现成了黑盒操作,于是 LLVM 看到的只是对某个栈上值的黑盒操作。生成的汇编先把常量加载进寄存器,再放到栈上,然后通过黑盒vld1_u32_x4加载回寄存器。修复方法是去掉vld1_u32_x4的黑盒实现,把它改成普通加载的兼容包装,让编译器能够正常优化。非常感谢 Rust stdarch 维护者 Folkert de Vries 帮忙调查并实现了这个修复。那我们把所有 intrinsics 都改成普通编译器操作的包装,不就行了?想得美。那个vld1_u32_x4并不是真正的黑盒。它是一个硬件操作,只是还没人给它写过优化 pass。而如果你想把某种奇特的操作用编译器能理解的基本块表达出来(或者只是想对几条略有差异的硬件指令的共同行为做抽象),这个操作就得由好几个基本块组成。这对 Rust 特别有吸引力,因为这样支持 LLVM 以外的后端会更容易,不过 Clang 也一直在朝这个方向走。但要用几个积木块拼出想要的操作,就需要优化器把它们重新组合成一条指令。无关的优化模式很容易破坏这一点,比如重排这些块、打断模式匹配。于是这些优化有时在简单的测试用例上能工作,到了真实代码里就失效。所以 SIMD intrinsics 被困在一场无休止的拉锯战中:一边要 lowering 成预期的指令,一边又要配合预期的编译器优化。除了这些根本性的限制,还有编译器指令选择上的 bug。我遇到过 LLVM 看到一个带常量索引的 512 位向量 shuffle 操作,然后说「哦,我知道,这个我能优化!」,可它的「优化」用的是 SSE4.2 时代的操作,比直接执行我要求的那条 shuffle 指令慢得多。(在我报告之后,这个问题在 LLVM 23 里修好了。)还有把一条唯一目的就是高效编码的 intrinsic lowering 成效率更低的编码。我手上真有一份自己发现的这类编译器 bug 清单。Rust 特有的那些在我报告后修好了,但还有一堆影响所有 Rust、C++ 和 C 的 LLVM bug 仍未修复。另外,如果你在想——不,这不只是 LLVM 的问题。GCC 有类似的毛病,而 MSVC 在这方面的表现明显比这两个主流开源编译器都差。附赠一个有趣的事实: Intel 忘了把一些 AVX-512 指令放进他们可搜索的 Intrinsics Guide,于是大多数编译器都没有实现它们,现在你没法从高级语言里用到这些指令。我已经把它们贡献给了 rustc,但还没在 stable 上发布。
内联汇编
你可能会觉得这样就能绕开编译器,躲过 intrinsics 的所有问题。某种程度上确实可以,但代价是你的内联汇编块变成了一个彻头彻尾的黑盒。不仅所有优化问题卷土重来,进出内联汇编块本身也有开销。如果你想在里面写一个较大的函数,并且愿意牺牲编译器优化,那还行得通;但如果只是想用一两条指令,就不划算了。
编译器功能愿望清单
长话短说,按重要性排序:
在标准库方面,我希望看到类似 crater 的机制来验证 intrinsics 的改动,并且希望 std::simd 能在 stable 上可用,这样生态里的 crate 就能删掉大部分代码,同时获得对所有冷门平台的支持。
结论
Rust 生态对 SIMD 的支持已经成熟了很多。虽然有些地方仍有改进空间(尤其是三角函数),但编译器功能和库生态近来的进展,让 Rust 即便在内存安全不是硬性要求时,对 SIMD 代码也很有吸引力。
加时赛:硬件现状
在为 5 种不同的指令集写过可移植 SIMD 代码之后,我很有想法。
x86
这是一团乱麻,而这一切都要怪 Intel。AVX2 既是最常见的指令集,也是我用过的最坑的指令集。每次我想实现一个简单直接的 SIMD 操作,有一半的情况 AVX-512 和 NEON 都原生支持,唯独 AVX2 需要又复杂又慢的模拟。它压根算不上一个正经的 256 位 ISA,更像是两个 128 位执行单元硬拼在一起,这一点更是雪上加霜。但还有更糟的。虽说搭载 AVX2 的 CPU 2013 年就上市了,可最后一款不带 AVX2 的 Intel CPU 居然是 2021 年发布的!所以你根本不能指望 AVX2 一定存在,那就祝你好运,去凑合用……翻一下笔记……2008 年的 SSE4.2 吧!这就是为什么到了 2026 年,Firefox 硬件调查里仍有 15% 的 x86 CPU 不支持 AVX2。就为了它们,去写和维护 SSE4.2 的代码路径吧,玩得开心!今年是哪一年?!Intel 在 2015 年推出了 AVX-512,修掉了 AVX2 的一堆荒唐之处,然后……就是没把它放进任何 CPU 里?它基本上只出现在服务器上,其他所有人只能困在 AVX2 甚至 SSE4.2 上。于是 Intel 就有了三套完全不同的 SIMD 扩展同时并存!不过等等,还有更糟的!在早期支持 AVX-512 的 Intel CPU 上运行 AVX-512 指令,哪怕只用一个核心,也会降低所有核心的频率。任何地方有 AVX-512 负载,都会拖累整颗芯片其余部分的性能!讽刺的是,AVX-512 偏偏只出现在核心数很多的高端 CPU 上,而这类 CPU 受这种连带影响尤其严重!你可能会想,在这些 CPU 上只要长时间全核运行 AVX-512,总还能捞到点好处吧,但那样一来瓶颈又变成了内存,CPU 在干什么都无所谓了。所以在这些 CPU 上,AVX-512 实际上带不来任何性能提升,还常常拖后腿,只有在不碰内存的人造微基准测试里才好看。这实在太可惜了,因为除此之外 AVX-512 相比 AVX2 是巨大的进步。512 位的宽度先放一边,光是把那套合理的操作集给我就够了!这种降频行为直到 2019 年的 Ice Lake 架构才被修好。不是彻底修好,但足以让 AVX-512 总体上划算。这就是为什么 fearless_simd 只在 Ice Lake 及之后的型号上支持 AVX-512,以及支持 AMD——AMD 压根就没有降频问题。AMD 用 Zen 4 展示了 Intel 把这件事搞砸得有多彻底:Zen 4 甚至没有硬件级的 512 位操作,它把大多数 512 位操作映射到 256 位执行单元上,结果在基准测试里照样吊打 Intel 的原生 512 位硬件。到了 Zen 5,原生 512 位硬件一上,胜负彻底定了。难怪 Intel 最近这么吃力。倒不是说 AMD 就毫无责任。我们没法用 scatter/gather 指令就是因为他们:在 Zen 上这些指令没有硬件原生实现,结果比发一堆小加载还慢。Intel 在早期 AVX2 CPU 上也把 scatter/gather 做得比标量加载还慢,但他们后来总算(某种程度上)改好了;AMD 连试都没试。LLVM 在对代码做自动向量化时,有时会为 AVX-512 生成 scatter/gather 指令,这在 Intel 上会让性能降低 1.75 倍,在 AMD 上降低 4 倍,所以我都不确定 LLVM 图什么。我记得要触发这个问题得传 -C target-cpu=,但我没有大规模测试过。Steam 硬件调查显示 23.9% 的系统支持 AVX-512。这个数字偏向高端/游戏系统;比如 Firefox 显卡调查里那 15% 只有 SSE4.2 的系统,在这里只占 2%。调查还按 AVX-512 的可选特性做了细分,从中可以推断出 23.95%(avx512vnni)减去 23.90%(基线 avx512f)等于 -0.05% 的系统拥有慢得离谱的 AVX-512。这个百分比为什么是负的,你猜得到原因我也猜得到。所以至少在桌面端,那种残缺的 AVX-512 并不存在,也就是说你不用操心它。因此把 Ice Lake 设为 AVX-512 的要求不会让你损失什么,还能换来一些有用的指令,不过用基线版 AVX-512 也不算太糟。关于 Skylake 服务器的普及率没有公开数据,这类服务器有 AVX-512 但会拖慢性能。把 Ice Lake 设为 AVX-512 的要求,让 Skylake 退回用 AVX2,应该能避免这种性能下降。Intel 差那么一点点就又要把事情搞砸了:他们想用 AVX10 取代 AVX-512,而 AVX10 就是 AVX-512,只不过向量宽度要么是 256 位要么是 512 位,而且你不知道是哪种。好在 AMD 那套明显更优的设计——直接把 512 位向量映射到 256 位硬件上——避免了这场灾难,逼着 Intel 回到合理的编程模型。呼。谢了,AMD。今年,AVX-512 终于要借着改名成 AVX 10.2,在即将推出的 Intel CPU 上成为强制要求。这正是我们一直想要的。嗯,不是指改名这件事。另外,对非常接近零的浮点数做运算会让性能暴跌。AMD 在这类运算上大约慢 2 倍,而在 Intel 上你会遇到 30 倍的减速。不知怎的,每次我了解到关于 SIMD 的可怕事实,罪魁祸首总是 Intel。
ARM
AVX2 做错的一切,64 位 NEON 都做对了。它只有 128 位向量,你会以为它相当于 SSE4.2,但实际上它更接近 AVX2。NEON 的寄存器空间和 AVX2 一样大,而这在实践中往往是瓶颈所在。而且它不需要你显式地同时应付两个并排的 128 位执行单元:强力的 ARM 核心通过指令级并行透明地并行执行 128 位操作,而功耗受限的廉价核心仍然可以逐个执行。NEON 还加入了不多不少的一批大于 128 位的指令,让常见操作开箱即用。在我做的 SIMD base64 解码测试中,同一个算法在 NEON 上比在 AVX2 上快 1.5 到 2 倍(但仍比 AVX-512 慢 2 倍)。而这一切都建立在单一、简单的编程模型之上,而不是好几套不同的模型。它在 64 位 ARM 芯片中是强制的,不需要 multiversioning!我对 Aarch64 NEON 唯一能提出的批评是:同一颗芯片上有两类核心(“性能核”和“能效核”),执行特性完全不同,所以在性能核上快的指令序列在能效核上就慢,反之亦然。因此,即使你明确知道自己针对的是哪款 CPU,也无法真正挑出一个最优实现,一切都是取舍!再考虑到市面上 ARM CPU 的多样性,情况只会更糟。好在 NEON 有足够多的操作直接以硬件指令实现,性能也说得过去,才没让这件事变成彻头彻尾的噩梦。与此同时,SVE 基本没什么用。SVE2 现在在 ARM CPU 中是强制的,但即便在高端服务器芯片上它也是以 128 位宽度实现的,所以它只是一个多了几道手续的别扭 NEON。从技术上说过,云服务器 ARM 芯片曾有一代实现了 256 位 SVE,但那是 SVE 不是 SVE2,而且在云上你反正直接用 AVX-512 就行了。也许还得再等十年左右,等 256 位 SVE2 硬件普及开来,才能看到它的天才之处,但眼下——别费这个劲。ARM 没有能对抗 AVX-512 的东西,后者寄存器空间大 4 倍,还有四个 512 位执行单元,一次能处理 2048 位。但考虑到 ARM 的目标市场,以及优秀的 AVX-512 最终也会被内存带宽卡住,我不觉得 ARM 需要这么一个东西。AVX-512 的主要好处是支持的操作范围宽得多,而 NEON 已经有了。
RISC-V
RISC-V 向量指令(RVV)根本不值一提,因为支持向量的 RISC-V 硬件本身就不值一提。RISC-V 在廉价微控制器领域一路高歌,但 2026 年支持向量的 RISC-V 硬件性价比惨不忍睹。也许 Tenstorrent 在 2028 年左右真正流片之后会改变这一局面,但 2026 年你完全不必操心。就算有像样的硬件,规范本身的写法也让某些关键指令没法被编译器使用。单这一点就把性能拖到荒唐的地步,除非你去折腾那些冷门编译器标志。
其他
剩下那些支持 SIMD 的架构都太冷门,除非有人花钱请你做,否则不值得考虑——POWER 和 s390x 是 IBM 和/或银行在掏钱,LoongArch 则是中国政府在掏钱。std::simd 至今仍在 64 位 SPARC 上跑 CI,但没人会花钱请你支持它。