【发布时间】:2014-07-02 07:06:28
【问题描述】:
Arstechnia 最近有一篇文章Why are some programming languages faster than others。它比较了 Fortran 和 C 并提到了求和数组。在 Fortran 中,假设数组不重叠,以便进一步优化。在 C/C++ 中,指向同一类型的指针可能会重叠,因此通常不能使用这种优化。但是,在 C/C++ 中,可以使用 restrict 或 __restrict 关键字告诉编译器不要假设指针重叠。所以我开始研究这个关于自动矢量化的问题。
以下代码在 GCC 和 MSVC 中向量化
void dot_int(int *a, int *b, int *c, int n) {
for(int i=0; i<n; i++) {
c[i] = a[i] + b[i];
}
}
我在有和没有重叠数组的情况下对此进行了测试,它得到了正确的结果。但是,我使用 SSE 手动对该循环进行矢量化的方式无法处理重叠数组。
int i=0;
for(; i<n-3; i+=4) {
__m128i a4 = _mm_loadu_si128((__m128i*)&a[i]);
__m128i b4 = _mm_loadu_si128((__m128i*)&b[i]);
__m128i c4 = _mm_add_epi32(a4,b4);
_mm_storeu_si128((__m128i*)c, c4);
}
for(; i<n; i++) {
c[i] = a[i] + b[i];
}
接下来我尝试使用__restrict。我假设由于编译器可以假设数组不重叠,因此它不会处理重叠数组,但 GCC 和 MSVC 即使使用__restrict 仍然可以得到重叠数组的正确结果。
void dot_int_restrict(int * __restrict a, int * __restrict b, int * __restrict c, int n) {
for(int i=0; i<n; i++) {
c[i] = a[i] + b[i];
}
}
为什么带有和不带有__restrict 的自动矢量化代码会得到重叠数组的正确结果?。
这是我用来测试的完整代码:
#include <stdio.h>
#include <immintrin.h>
void dot_int(int *a, int *b, int *c, int n) {
for(int i=0; i<n; i++) {
c[i] = a[i] + b[i];
}
for(int i=0; i<8; i++) printf("%d ", c[i]); printf("\n");
}
void dot_int_restrict(int * __restrict a, int * __restrict b, int * __restrict c, int n) {
for(int i=0; i<n; i++) {
c[i] = a[i] + b[i];
}
for(int i=0; i<8; i++) printf("%d ", c[i]); printf("\n");
}
void dot_int_SSE(int *a, int *b, int *c, int n) {
int i=0;
for(; i<n-3; i+=4) {
__m128i a4 = _mm_loadu_si128((__m128i*)&a[i]);
__m128i b4 = _mm_loadu_si128((__m128i*)&b[i]);
__m128i c4 = _mm_add_epi32(a4,b4);
_mm_storeu_si128((__m128i*)c, c4);
}
for(; i<n; i++) {
c[i] = a[i] + b[i];
}
for(int i=0; i<8; i++) printf("%d ", c[i]); printf("\n");
}
int main() {
const int n = 100;
int a[] = {1,1,1,1,1,1,1,1};
int b1[] = {1,1,1,1,1,1,1,1,1};
int b2[] = {1,1,1,1,1,1,1,1,1};
int b3[] = {1,1,1,1,1,1,1,1,1};
int c[8];
int *c1 = &b1[1];
int *c2 = &b2[1];
int *c3 = &b3[1];
dot_int(a,b1,c, 8);
dot_int_SSE(a,b1,c,8);
dot_int(a,b1,c1, 8);
dot_int_restrict(a,b2,c2,8);
dot_int_SSE(a,b3,c3,8);
}
输出(来自 MSVC)
2 2 2 2 2 2 2 2 //no overlap default
2 2 2 2 2 2 2 2 //no overlap with manual SSE vector code
2 3 4 5 6 7 8 9 //overlap default
2 3 4 5 6 7 8 9 //overlap with restrict
3 2 2 2 1 1 1 1 //manual SSE vector code
编辑:
这是另一个生成更简单代码的插入版本
void dot_int(int * __restrict a, int * __restrict b, int * __restrict c, int n) {
a = (int*)__builtin_assume_aligned (a, 16);
b = (int*)__builtin_assume_aligned (b, 16);
c = (int*)__builtin_assume_aligned (c, 16);
for(int i=0; i<n; i++) {
c[i] = a[i] + b[i];
}
}
【问题讨论】:
-
您的测试用例没有处理别名输入数据会出现问题的情况。您需要输入和输出指针别名为同一个数组并且加载存储顺序很重要的情况。
-
@PaulR,我认为这就是我所做的。我将输出添加到问题的末尾。输出
c数组与输入b数组重叠。手动 SSE 代码失败的原因是加载存储顺序很重要。 -
好的 - 抱歉 - 也许我看错了你的代码。
-
你怎么知道编译器正在向量化代码?编译器为这个函数生成什么向量化代码?
-
@Apriori,编译器 GCC 和 MSVC,报告他们向量化循环。我就是这样知道的。该程序集比我预期的要长得多,但乍一看表明编译器生成了一个矢量化和非矢量化版本,然后根据数组是否重叠(内存地址和范围)决定采用哪条路径。但这仍然是一个猜测,我明天有更多时间检查。
标签: c++ c optimization sse auto-vectorization