SSE 是多余的还是气馁的?

Is SSE redundant or discouraged?

环顾四周和互联网,我可以找到很多关于现代编译器在许多实际情况下击败 SSE 的帖子,我刚刚在我继承的一些代码中遇到了当我禁用一些 2006 年编写的 SSE 代码时基于整数的图像处理并强制代码沿标准 C 分支向下运行,它运行得更快。

在具有多核和高级流水线等的现代处理器上,较旧的 SSE 代码是否表现不佳 gcc -O2

您很可能会看到现代编译器使用 SSE4。但是,即使他们坚持使用同一个 ISA,他们通常也更擅长调度。让 SSE 单位保持忙碌意味着仔细管理数据流。

内核无关紧要,因为每个指令流(线程)都在单个内核上运行。

是的——但主要是因为不鼓励编写内联汇编。

SSE 指令(和其他向量指令)已经存在了足够长的时间,编译器现在已经很好地理解了如何使用它们来生成高效代码。

除非您清楚自己在做什么,否则您不会比编译器做得更好。即便如此,通常也不值得为击败编译器而付出努力。即便如此,我们为一个特定的 CPU 优化的努力可能不会为其他 CPU 产生好的代码。

你必须小心微基准测试。 真的 很容易衡量与您想象的不同的东西。就 L1 I-cache / uop-cache 和分支预测器条目的压力而言,微基准测试通常根本不考虑代码大小。

在大多数情况下,微基准测试通常会尽可能准确地预测所有分支,而经常调用但不在紧密循环中的例程在实践中可能效果不佳。


多年来,SSE 增加了许多内容。新代码的合理基线是 SSSE3(在 Intel Core2 及更高版本以及 AMD Bulldozer 及更高版本中找到),只要存在标量回退即可。添加快速字节洗牌 (pshufb) 是某些事情的游戏规则改变者。 SSE4.1 也为整数代码添加了很多不错的东西。如果旧代码不使用它,编译器输出或新的手写代码可以做得更好。

目前我们正在使用 AVX2,它在 256b 寄存器中同时处理两个 128b 通道。有一些 256b 随机播放指令。 AVX/AVX2 提供所有先前 SSE 指令的 3 操作数(非破坏性 dest、src1、src2)版本,这有助于提高代码密度,即使使用 256b 操作的双通道方面是一个缺点(或当目标时整数代码没有 AVX2 的 AVX1)。

一两年后,第一个 AVX512 桌面硬件可能会出现。这增加了大量强大的功能(屏蔽寄存器,并填补了高度非正交的 SSE / AVX 指令集中的更多空白),以及更宽的寄存器和执行单元。


如果旧的 SSE 代码在编写时仅比标量代码提供了边际加速,或者没有人对其进行基准测试,那可能就是问题所在。编译器的进步可能会导致为标量 C 生成的代码击败需要大量改组的旧 SSE。有时,将数据改组到向量寄存器中的成本会耗尽所有速度的加速。

或者根据您的编译器选项,编译器甚至可能会自动向量化。 IIRC,gcc -O2 不启用 -ftree-vectorize,所以你需要 -O3 来实现 auto-vec。


另一件可能阻碍旧 SSE 代码的事情是它可能假设未对齐的 loads/stores 很慢,并使用 palignr 或类似的技术在寄存器中的未对齐数据和对齐的 [=47] 之间切换=].因此,旧代码可能会以一种在最近的代码上实际上更慢的方式针对旧的微体系结构进行调整。

因此,即使不使用以前不可用的任何指令,针对不同的微体系结构进行调整也很重要。


编译器输出很少是最优的,尤其是。如果你没有告诉它关于指针没有别名(restrict),或者被对齐。但它经常设法 运行 相当快。您通常可以对其进行一些改进(尤其是通过减少 uops/insns 来完成相同的工作,从而对超线程更加友好),但是您必须 know the microarchitecture you're targeting. E.g. Intel Sandybridge and later can only micro-fuse memory operands with one-register addressing mode. Other links at the wiki。


所以要回答这个问题,SSE 指令集绝不是多余的或令人沮丧的。不鼓励将它直接与 asm 一起用于随意使用(请改用内在函数)。除非您实际上可以加速编译器输出,否则不鼓励使用内部函数。如果它们现在并列,那么未来的编译器将更容易处理标量代码,而不是处理向量内在函数。

补充一下 Peter's already excellent ,要考虑的一个基本点是,编译器并不了解程序员所知道的关于问题域的所有信息,并且通常程序员没有简单的方法来表达有用的约束和其他相关信息,真正智能的编译器可能能够利用这些信息来帮助矢量化。这在很多情况下可以给程序员带来巨大的优势。

例如,对于一个简单的案例,例如:

// add two arrays of floats

float a[N], b[N], c[N];

for (int i = 0; i < N; ++i)
    a[i] = b[i] + c[i];

任何体面的编译器都应该能够用 SSE/AVX/whatever 很好地完成矢量化工作,用 SIMD 内在函数实现它没有什么意义。除了数据对齐或 N 值的可能范围等相对较小的问题外,编译器生成的代码应该接近最佳。

但是如果你有一些不那么直接的东西,例如

// map array of 4 bit values to 8 bit values using a LUT

const uint8_t LUT[16] = { 0, 1, 3, 7, 11, 15, 20, 27, ..., 255 };
uint8_t in[N];   // 4 bit input values
uint8_t out[N];  // 8 bit output values

for (int i = 0; i < N; ++i)
    out[i] = LUT[in[i]];

你不会从你的编译器中看到任何自动向量化,因为 (a) 它不知道你可以使用 PSHUFB 来实现一个小的 LUT,并且 (b) 即使它知道,它无法知道输入数据被限制在 4 位范围内。因此,程序员可以编写一个简单的 SSE 实现,这很可能会快一个数量级:

__m128i vLUT = _mm_loadu_si128((__m128i *)LUT);
for (int i = 0; i < N; i += 16)
{
    __m128i va = _mm_loadu_si128((__m128i *)&b[i]);
    __m128i vb = _mm_shuffle_epi8(va, vLUT);
    _mm_storeu_si128((__m128i *)&a[i], vb);
}

也许再过 10 年,编译器将足够聪明来做这种事情,编程语言将有方法来表达程序员所知道的关于问题、数据和其他相关约束的一切,在这一点上它可能是像我这样的人考虑新职业的时候了。但在那之前,仍然会存在一个大问题 space,人类仍然可以通过手动 SIMD 优化轻松击败编译器。

这是两个独立且严格来说不相关的问题:

1) SSE 和特别是 SSE 调整的代码库是否过时/"discouraged"/退休?

简单回答:还没有,也不是真的。高层原因:因为周围仍然有足够多的硬件(即使在 HPC 领域,人们可以很容易地在其中找到 Nehalem)仅搭载 SSE*,但没有可用的 AVX*。如果您不看 HPC,那么请考虑例如 Intel Atom CPU,它目前仅支持 SSE4。

2) 为什么 gcc -O2(即自动矢量化,运行 在仅限 SSE 的硬件上)比 9 年前编写的一些旧的(可能是内在的)SSE 实现更快。

回答:视情况而定,但首先,编译器方面的改进非常积极。在过去的 9 年中,AFAIK 排名前 4 位的 x86 编译器开发团队对自动矢量化或显式矢量化领域进行了 巨额投资 。他们这样做的原因也很清楚:在过去的 9 年中,SIMD "FLOPs" x86 硬件的潜力已经(正式地)增加 "by 8 times"(即 SSE4 峰值触发器的 8 倍)。

让我自己再问一个问题:

3) 好的,SSE 并没有过时。但它会在 X 年后过时吗?

答案:谁知道呢,但至少在 HPC 中,随着更广泛的 AVX-2 和 AVX-512 兼容硬件的采用,SSE 内在函数代码库很可能很快就会退役,尽管这又取决于您开发的内容。一些低级优化的 HPC/HPC+ 媒体库可能会长时间保持高度调整的 SSE 代码路径。