我最近深入研究了 x86-64 架构并探索了 SSE 和 AVX 的功能。我尝试编写一个简单的向量加法函数,如下所示:
void compute(const float *a, const float *b, float *c) {
c[0] = a[0] + b[0];
c[1] = a[1] + b[1];
c[2] = a[2] + b[2];
c[3] = a[3] + b[3];
}
同时使用
gcc
和 clang
,我使用以下选项进行编译:
cc -std=c23 -march=native -O3 -ftree-vectorize main.c
但是,当我检查反汇编时,输出在矢量化方面并不完全符合我的预期:
compute:
vmovss xmm0, dword ptr [rdi]
vaddss xmm0, xmm0, dword ptr [rsi]
vmovss dword ptr [rdx], xmm0
vmovss xmm0, dword ptr [rdi + 4]
vaddss xmm0, xmm0, dword ptr [rsi + 4]
vmovss dword ptr [rdx + 4], xmm0
vmovss xmm0, dword ptr [rdi + 8]
vaddss xmm0, xmm0, dword ptr [rsi + 8]
vmovss dword ptr [rdx + 8], xmm0
vmovss xmm0, dword ptr [rdi + 12]
vaddss xmm0, xmm0, dword ptr [rsi + 12]
vmovss dword ptr [rdx + 12], xmm0
ret
这看起来像标量代码,一次处理一个元素。但是当我手动使用内在函数时,我得到了预期的向量化实现:
#include <xmmintrin.h>
void compute(const float *a, const float *b, float *c) {
__m128 va = _mm_loadu_ps(a);
__m128 vb = _mm_loadu_ps(b);
__m128 vc = _mm_add_ps(va, vb);
_mm_storeu_ps(c);
}
据我了解,现代处理器非常强大,SSE(1999 年推出)和 AVX(自 2011 年起)现已成为标准。然而,即使我明确启用优化,编译器似乎并不总是自动充分利用这些指令。
感觉有点像我们发明了隐形传送,但人们仍然更喜欢乘船穿越大西洋。现代编译器可能会犹豫是否为如此简单的事情生成矢量化代码,是否有合理的理由?
别名就是这里的问题。编译器无法知道
a
、b
和 c
相关的内存区域是否可以重叠。编译器有时可以生成特定的代码,但在这里,不值得检查是否存在任何重叠:它会使函数变慢。此外,标量操作在这里并没有那么糟糕,因为它们具有良好的流水线性(与矢量化操作相反)。编译器只是不知道该函数是否用于延迟受限的代码或吞吐量受限的代码中。
据我所知,C++(甚至 C++23)不支持
restrict
关键字。解决此问题的一种方法是使用编译器扩展(请参阅此相关帖子)。您可以支持 MSVC、Clang 和 GCC,并根据目标编译器定义不同的宏。
另一种解决方案是使用实验性 SIMD C++ TS。这至少比使用内在函数编写不可移植的 SIMD 代码更干净(而且更具可读性)。