SSE是多余还是气馁?

时间:2015-11-27 00:23:09

标签: optimization sse simd auto-vectorization

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

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

5 个答案:

答案 0 :(得分:8)

您必须小心使用微基准测试。 根据L1 I-cache / uop-cache和branch-predicictor条目的压力,Microbenchmarks通常根本不考虑代码大小。

在大多数情况下,微基准测试通常会预测所有分支,尽管它们可以被频繁调用而不是紧密循环,但在实践中可能效果不佳。

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

目前,我们需要AVX2,它可以同时处理两个128b通道,256b寄存器。有一些256b shuffle指令。 AVX / AVX2提供了所有先前SSE指令的3操作数(非破坏性dest,src1,src2)版本,这有助于提高代码密度,即使使用256b操作的双通道方面是缺点(或者在没有指向AVX1的情况下) AVX2表示整数代码。

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

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

或者根据您的编译器选项,编译器甚至可能是自动向量化。 IIRC,gcc -O2无法启用-ftree-vectorize,因此您需要-O3来进行自动转发。

可能阻碍旧SSE代码的另一件事是它可能假设未对齐的加载/存储很慢,并使用palignr或类似的技术在寄存器中的未对齐数据和对齐的加载/存储之间进行。所以旧的代码可能会以一种在最近的方式上实际上较慢的方式调整为旧代码。

因此,即使不使用以前无法使用的任何指令,调整不同的微体系结构也很重要。

编译器输出很少是最佳的,尤其是如果你还没有告诉它指针没有别名(restrict),或者是对齐的。但它经常设法运行得非常快。你可以经常改进它(特别是通过减少uops / insns来做同样的工作,更加超线程友好),但是你必须know the microarchitecture you're targeting。例如。英特尔Sandybridge及更高版本只能通过单寄存器寻址模式对存储器操作数进行微熔合。 wiki上的其他链接。

因此,为了回答标题,SSE指令集绝不是多余的或不鼓励的。不建议随意使用asm,以便随意使用(改为使用内在函数)。除非您实际上可以通过编译器输出获得加速,否则不鼓励使用内在函数。如果它们现在被捆绑在一起,将来的编译器使用标量代码更容易做得比使用矢量内在函数做得更好。

答案 1 :(得分:7)

只是要添加到Peter's已经很好answer,需要考虑的一个基本点是编译器不知道程序员对问题域的所有知识,并且通常没有简单的方法程序员表达有用的约束和其他相关信息,真正的智能编译器可能会利用这些信息以帮助矢量化。在许多情况下,这可以给程序员带来巨大的优势。

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

// 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 /其他方式做一个相当好的矢量化,并且用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优化轻松击败编译器,这将是一个巨大的问题空间。

答案 2 :(得分:2)

这是两个独立且严格无关的问题:

1)SSE一般和SSE调优的代码库特别是过时/“沮丧”/退休吗?

简要回答:还没有,不是真的。高级别原因:因为周围仍有足够的硬件(即使在HPC域中,人们很容易找到Nehalem),其中只有SSE *,但没有AVX *可用。如果你看看HPC以外,那么考虑一下英特尔 Atom CPU ,目前只支持SSE4。

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

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

让我自己再问一个问题:

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

答案:谁知道,但至少在HPC中,采用更广泛的AVX-2和AVX-512兼容硬件,SSE内在代码库很有可能很快退出,尽管它又取决于你发展了什么。一些低级优化的HPC / HPC +媒体库可能会长时间保持高度优化的SSE代码路径。

答案 3 :(得分:0)

您可能会很好地看到现代编译器使用SSE4。但即使他们坚持使用相同的ISA,他们在安排时通常要好得多。保持SSE单元繁忙意味着仔细管理数据流。

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

答案 4 :(得分:0)

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

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

除非你知道自己在做什么,否则你不会比编译器做得更好。即便如此,它往往不值得花费在尝试击败编译器上的努力。即便如此,我们优化一个特定CPU的努力可能不会为其他CPU带来良好的代码。