4

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

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

4

5 回答 5

10

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

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


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

目前我们达到了 AVX2,它在 256b 寄存器中一次处理两个 128b 通道。有一些 256b shuffle 指令。AVX/AVX2 提供了所有先前 SSE 指令的 3 操作数(非破坏性 dest、src1、src2)版本,即使在使用 256b ops 的双通道方面存在缺点时(或针对 AVX1 没有AVX2 用于整数代码)。

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


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

或者根据您的编译器选项,编译器甚至可能是自动矢量化的。IIRC,gcc -O2不启用-ftree-vectorize,所以你需要-O3auto-vec。


可能阻碍旧 SSE 代码的另一件事是,它可能假设未对齐的加载/存储很慢,并且使用palignr或类似的技术在寄存器中的未对齐数据和对齐的加载/存储之间进行传输。因此,旧代码可能会针对旧微架构进行调整,而这种方式实际上在最近的微架构上更慢。

因此,即使不使用以前不可用的任何指令,调整不同的微架构也很重要。


编译器输出很少是最佳的,尤其是。如果你还没有告诉它指针不是别名 ( restrict),或者是对齐的。但它通常设法运行得非常快。您通常可以对其进行一些改进(尤其是通过减少 uops/insns 来完成相同的工作,从而对超线程更加友好),但您必须了解您所针对的微架构。例如,英特尔 Sandybridge 及更高版本只能使用单寄存器寻址模式对内存操作数进行微熔丝。 wiki上的其他链接。


因此,回答标题,SSE 指令集绝不是多余或不鼓励的。不鼓励随意使用它直接与 asm 一起使用(改用内部函数)。不鼓励使用内在函数,除非您实际上可以加速编译器输出。如果它们现在绑定在一起,那么未来的编译器使用标量代码做得比使用向量内在函数做得更好会更容易。

于 2015-11-27T00:51:35.017 回答
8

只是为了补充彼得已经很好的答案,要考虑的一个基本点是,编译器并不知道程序员所知道的关于问题域的所有内容,而且程序员通常没有简单的方法来表达有用的约束和其他相关信息一个真正聪明的编译器可能能够利用它来帮助向量化。在许多情况下,这可以给程序员带来巨大的优势。

例如,对于一个简单的情况,例如:

// 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 年,编译器就会足够聪明,可以做这种事情,而编程语言将有方法来表达程序员所知道的关于问题、数据和其他相关约束的一切,到那时可能是时候像我这样的人考虑新的职业。但在此之前,仍然存在很大的问题空间,人类仍然可以通过手动 SIMD 优化轻松击败编译器。

于 2015-11-27T08:45:18.510 回答
3

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

1) 一般 SSE 和特别是 SSE 调整的代码库是否过时/“不鼓励”/退休?

简要回答:还没有,也没有。高级原因:因为周围仍然有足够的硬件(即使在 HPC 域中,也可以轻松找到 Nehalem),这些硬件只有 SSE*,但没有可用的 AVX*。如果您查看 HPC 之外的内容,请考虑例如 Intel Atom CPU,它目前仅支持 SSE4。

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

:这取决于,但首先在编译器方面正在积极改进。AFAIK 前 4 名 x86 编译器开发团队在过去 9 年中对自动矢量化或显式矢量化领域进行了大量投资。他们这样做的原因也很清楚:在过去的 9 年中,x86 硬件中的 SIMD“FLOPs”潜力已经(正式地)增加了“8 倍”(即 SSE4 峰值 flops 的 8 倍)。

让我自己再问一个问题:

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

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

于 2015-11-30T20:23:07.550 回答
0

您很可能会看到现代编译器使用 SSE4。但即使他们坚持使用相同的 ISA,他们通常在调度方面也做得更好。让 SSE 单元保持忙碌意味着对数据流的仔细管理。

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

于 2015-11-27T00:32:59.090 回答
0

是的——但主要与不鼓励编写内联汇编的意义相同。

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

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

于 2015-11-27T00:36:53.610 回答