为什么循环没有被矢量化?
Why is the loop not being vectorized?
我知道只有当被访问的对象在内存中是连续的时才能进行矢量化。我创建了一个具有指针的结构,然后我创建了该结构的一个向量,并确保对象向量内的指针指向连续的数据块,我将它们设置为指向具有相同大小的双精度向量中的元素.
#include <iostream>
#include <vector>
struct Vec {
Vec() {}
double* a;
};
int main(int argc, char* argv[]) {
std::vector<double> vec_double(10000000, 1.0);
std::vector<Vec> vec_vec(10000000);
for (unsigned i = 0; i < 10000000; ++i)
vec_vec[i].a = &(vec_double[i]);
// Why is this loop not vectorized
for (unsigned i = 0; i < 10000000; ++i)
vec_double[i] += *(vec_vec[i].a);
double sum = 0.0;
for (unsigned i = 0; i < 10000000; ++i)
sum += vec_double[i];
std::cout << sum << std::endl;
return 0;
}
但是,即使使用 O3 优化,第 16 行的循环也没有被向量化。有人可以解释为什么会这样吗?
这里只是猜测,但是当只看这个具体的循环时,编译器并不知道vec_vec[i].a
指向vec_vec[i+1].a
旁边的内存位置。因此,如果不分别取消引用每个 .a
成员,它就无法进行计算。
看上面的循环就知道了。但如果它这样做,它也可以查看下面的循环,计算最终结果并打印出来。
你应该先从迭代器中获取原始指针。像这样 vec_vec.begin()_Ptr
和 vec_double.begin()._Ptr
。像在传统中一样使用这些指针。最后声明它没有别名的方法。像这样__declspec(noalias)
。它应该用 msvc 在 windows 上解决问题。不过,我认为 GCC 上没有 noalias 属性。
首先,"vectorization can only take place if the objects being accessed are contiguous in memory"是不正确的。 很可能通过非连续内存访问对循环进行矢量化,甚至在像 Haswell 这样的较新 CPU 上也支持硬件级别,其中你有 v(p)gather* 指令支持 "random memory access pattern" 编码向量化。
然而,其次,具有非连续访问的矢量化代码通常非常低效。连续(在这种情况下也称为 "unit-stride")内存访问通常总是最好在向量代码中使用(尽管从内存带宽的角度来看,连续访问模式可能更糟,但这是一个单独的长话题)。所以你的优化技术是合适的。
现在,第三,访问模式只是 其他原因 中的一个,为什么编译器没有自动对循环进行矢量化,甚至在轻推它之后也没有对其进行矢量化使用一些 pragmas/etc。
在您给定的情况下,gcc 被给定代码混淆可能有两个原因:
- 指针。编译器无法安全地假设运行时指针的实际值是什么,并且必须假设更糟(假设 p[i] 和 p[i+1] 指向相同甚至冲突的内存位置,结果覆盖每个其他或做更糟糕的事情)。
- 只是代码复杂。在某些情况下,处理面向对象的代码,还涉及 STL 等,会使向量化器感到困惑。
这两个问题通常都可以使用 restrict 关键字(我猜不是你的情况)、#pragma ivdep 或 #pragma omp simd 很好地解决。
但是,从 GCC 版本 4 开始,pragma ivdep 和 simd 仅 "reliably" 受支持。9.
另一种方法是重写您的代码,使其不处理指针甚至向量,而只使用一些固定大小的 C 样式数组。当然,对于真正的 C++ 代码来说这是糟糕的建议,但在您给定的代码片段中它是可行的,因为您静态指定了数组大小。
我知道只有当被访问的对象在内存中是连续的时才能进行矢量化。我创建了一个具有指针的结构,然后我创建了该结构的一个向量,并确保对象向量内的指针指向连续的数据块,我将它们设置为指向具有相同大小的双精度向量中的元素.
#include <iostream>
#include <vector>
struct Vec {
Vec() {}
double* a;
};
int main(int argc, char* argv[]) {
std::vector<double> vec_double(10000000, 1.0);
std::vector<Vec> vec_vec(10000000);
for (unsigned i = 0; i < 10000000; ++i)
vec_vec[i].a = &(vec_double[i]);
// Why is this loop not vectorized
for (unsigned i = 0; i < 10000000; ++i)
vec_double[i] += *(vec_vec[i].a);
double sum = 0.0;
for (unsigned i = 0; i < 10000000; ++i)
sum += vec_double[i];
std::cout << sum << std::endl;
return 0;
}
但是,即使使用 O3 优化,第 16 行的循环也没有被向量化。有人可以解释为什么会这样吗?
这里只是猜测,但是当只看这个具体的循环时,编译器并不知道vec_vec[i].a
指向vec_vec[i+1].a
旁边的内存位置。因此,如果不分别取消引用每个 .a
成员,它就无法进行计算。
看上面的循环就知道了。但如果它这样做,它也可以查看下面的循环,计算最终结果并打印出来。
你应该先从迭代器中获取原始指针。像这样 vec_vec.begin()_Ptr
和 vec_double.begin()._Ptr
。像在传统中一样使用这些指针。最后声明它没有别名的方法。像这样__declspec(noalias)
。它应该用 msvc 在 windows 上解决问题。不过,我认为 GCC 上没有 noalias 属性。
首先,"vectorization can only take place if the objects being accessed are contiguous in memory"是不正确的。 很可能通过非连续内存访问对循环进行矢量化,甚至在像 Haswell 这样的较新 CPU 上也支持硬件级别,其中你有 v(p)gather* 指令支持 "random memory access pattern" 编码向量化。
然而,其次,具有非连续访问的矢量化代码通常非常低效。连续(在这种情况下也称为 "unit-stride")内存访问通常总是最好在向量代码中使用(尽管从内存带宽的角度来看,连续访问模式可能更糟,但这是一个单独的长话题)。所以你的优化技术是合适的。
现在,第三,访问模式只是 其他原因 中的一个,为什么编译器没有自动对循环进行矢量化,甚至在轻推它之后也没有对其进行矢量化使用一些 pragmas/etc。 在您给定的情况下,gcc 被给定代码混淆可能有两个原因:
- 指针。编译器无法安全地假设运行时指针的实际值是什么,并且必须假设更糟(假设 p[i] 和 p[i+1] 指向相同甚至冲突的内存位置,结果覆盖每个其他或做更糟糕的事情)。
- 只是代码复杂。在某些情况下,处理面向对象的代码,还涉及 STL 等,会使向量化器感到困惑。
这两个问题通常都可以使用 restrict 关键字(我猜不是你的情况)、#pragma ivdep 或 #pragma omp simd 很好地解决。 但是,从 GCC 版本 4 开始,pragma ivdep 和 simd 仅 "reliably" 受支持。9.
另一种方法是重写您的代码,使其不处理指针甚至向量,而只使用一些固定大小的 C 样式数组。当然,对于真正的 C++ 代码来说这是糟糕的建议,但在您给定的代码片段中它是可行的,因为您静态指定了数组大小。