为什么循环没有被矢量化?

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()_Ptrvec_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 被给定代码混淆可能有两个原因:

  1. 指针。编译器无法安全地假设运行时指针的实际值是什么,并且必须假设更糟(假设 p[i] 和 p[i+1] 指向相同甚至冲突的内存位置,结果覆盖每个其他或做更糟糕的事情)。
  2. 只是代码复杂。在某些情况下,处理面向对象的代码,还涉及 STL 等,会使向量化器感到困惑。

这两个问题通常都可以使用 restrict 关键字(我猜不是你的情况)、#pragma ivdep 或 #pragma omp simd 很好地解决。 但是,从 GCC 版本 4 开始,pragma ivdep 和 simd 仅 "reliably" 受支持。9.

另一种方法是重写您的代码,使其不处理指针甚至向量,而只使用一些固定大小的 C 样式数组。当然,对于真正的 C++ 代码来说这是糟糕的建议,但在您给定的代码片段中它是可行的,因为您静态指定了数组大小。