OpenMP并行C++平均矩阵 < >我有C++中的代码,它计算矩阵的每个列的平均值。我想使用OpenMP并行化代码 包括 包括 包括 包括 使用名称空间std; 向量平均常数向量与原向量{ vector resultoriginal.size,vectororiginal[0]。size; 向量平均原始值[0]。大小,0.0; 对于int,i=0;i

OpenMP并行C++平均矩阵 < >我有C++中的代码,它计算矩阵的每个列的平均值。我想使用OpenMP并行化代码 包括 包括 包括 包括 使用名称空间std; 向量平均常数向量与原向量{ vector resultoriginal.size,vectororiginal[0]。size; 向量平均原始值[0]。大小,0.0; 对于int,i=0;i,c++,matrix,parallel-processing,openmp,average,C++,Matrix,Parallel Processing,Openmp,Average,average[k]+=vector[k];不是原子运算 每个线程可能会,也可能会同时读取k的当前值,将其相加,然后写回该值 这些类型的跨线程数据竞争是未定义的行为 编辑: 一个简单的解决方法是反转循环的顺序,并在k循环上并行化。这样,每个线程将只写入一个值。但是,然后,您将用k乘以顶级向量上的查找次数,因此您可能不会获得太大的性能增益,因为您将开始非常猛烈地搅动缓存。Frank在s中是正确的您的直接问题可能是您正在使用非原子操作: average[k] += vector[k]; 您可以使用

average[k]+=vector[k];不是原子运算

每个线程可能会,也可能会同时读取k的当前值,将其相加,然后写回该值

这些类型的跨线程数据竞争是未定义的行为

编辑:
一个简单的解决方法是反转循环的顺序,并在k循环上并行化。这样,每个线程将只写入一个值。但是,然后,您将用k乘以顶级向量上的查找次数,因此您可能不会获得太大的性能增益,因为您将开始非常猛烈地搅动缓存。

Frank在s中是正确的您的直接问题可能是您正在使用非原子操作:

average[k] += vector[k];
您可以使用以下方法解决此问题:

#pragma omp atomic
average[k] += vector[k];
但一个更大的概念性问题是,这可能不会加快代码的速度。您正在执行的操作非常简单,并且您的内存至少行是连续的

事实上,我已经为您的代码做了一个最低限度的工作示例,您应该为您的问题这样做:

#include <vector>
#include <cstdlib>
#include <chrono>
#include <iostream>
using namespace std;

vector<float> average(const vector<vector<unsigned char>>& original){
  vector<float> average(original[0].size(), 0.0);

  #pragma omp parallel for
  for (int i=0; i<original.size(); i++) {
    const vector<unsigned char>& vector = original[i];
    for (int k = 0; k < vector.size(); ++k) {
      #pragma omp atomic
      average[k] += vector[k];
    }
  }
  for (float& val : average) {
    val /= original.size();
  }

  return average;
}

int main(){
  vector<vector<unsigned char>> mat(1000);
  for(int y=0;y<mat.size();y++)
  for(int x=0;x<mat.size();x++)
    mat.at(y).emplace_back(rand()%255);

  std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now();
  double dont_optimize = 0;
  for(int i=0;i<100;i++){
    auto ret = average(mat);
    dont_optimize += ret[0];
  }
  std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();

  std::cout<<"Time = "<<(std::chrono::duration_cast<std::chrono::microseconds>(end - begin).count()/100)<<std::endl;

  return 0;
}
请注意,我也使用float而不是double:您可以通过这种方式将两倍的数字填充到相同的空间中,这有利于缓存

这使得不使用OpenMP时的运行时间为292微秒,而使用OpenMP时的运行时间为9426微秒

总之,使用OpenMP/parallelism会降低代码的速度,因为并行执行的工作比设置并行所需的时间要短,但使用更好的内存布局会使速度提高约90%。此外,使用我创建的handy Matrix类可以提高代码的可读性和可维护性

编辑:

在10000x1000而不是1000x1000的矩阵上运行此操作会得到类似的结果。对于向量向量,不使用OpenMP时为7449微秒,使用OpenMP时为156316微秒。对于平面阵列索引,不使用OpenMP时为32668微秒,使用OpenMP时为145470微秒

性能可能与我的机器上可用的硬件指令集有关,特别是,如果我的机器缺少原子指令,则OpenMP将不得不使用互斥体等来模拟它们。事实上,在平面阵列示例中,使用-march=native编译OpenMP:33079 microseco的性能得到了改善,但仍然不太好没有OpenMP的nds和使用OpenMP的127841微秒。稍后我将使用更强大的机器进行实验

编辑

虽然上述测试是在IntelR CoreTM i5 CPU M 480@2.67GHz上执行的,但我在2.50GHz的恶劣IntelR XeonR CPU E5-2680 v3上编译了此代码,其-O3-march=native。结果类似:

1000x1000,矢量的矢量,不带OpenMP:145μs 1000x1000,矢量的矢量,带OpenMP:2941μs 10000x1000,矢量的矢量,不带OpenMP:20254μs 10000x1000,矢量的矢量,带OpenMP:85703μs 1000x1000,平面阵列,无OpenMP:139μs 1000x1000,平面阵列,带OpenMP:3171μs 10000x1000,平面阵列,无OpenMP:18712μs 10000x1000,平面阵列,带OpenMP:89097μs 这证实了我们之前的结果:使用OpenMP执行此任务往往会降低速度,即使您的硬件令人惊讶。事实上,两个处理器之间的大部分速度提高可能是由于Xeon的大L3缓存大小:30720K比i5上的3720K缓存大10倍

编辑

结合以下答案,我们可以有效地利用并行性:

vector<float> average(const Matrix& original){
  vector<float> average(original.width, 0.0);
  auto average_data = average.data();

  #pragma omp parallel for reduction(+ : average_data[ : original.width])
  for(int y=0;y<original.height;y++){
    for(int x=0;x<original.width;x++)
      average_data[x] += original(x,y);
  }

  for (float& val : average) 
    val /= original.height;

  return average;
}

对于24个线程,10000x1000阵列的速度为2629微秒:比串行版本提高了7.1x。在没有平面阵列索引的原始代码上使用Zulan的策略可以获得3529微秒,因此,通过使用更好的布局,我们仍然可以获得25%的加速。

Frank和Richard的基本问题是正确的。提示内存外布局也是如此。但是,它可能比使用原子更好。不仅原子数据访问非常昂贵,通过从所有线程写入完全共享的内存空间,缓存性能会下降。因此,只有原子增量的并行循环可能无法很好地扩展

减少 基本思想是首先计算一个局部和向量,然后安全地将这些向量相加。这样,大部分工作都可以独立高效地完成。最新的OpenMP版本使这项工作变得非常方便

下面是基于Richards示例的示例代码——不过我确实交换了索引并修复了操作符的效率

#include <chrono>
#include <cstdlib>
#include <iostream>
#include <memory>
#include <vector>

class Matrix {
public:
  std::vector<unsigned char> mat;
  int width;
  int height;
  Matrix(int width0, int height0) {
    srand(0);
    width = width0;
    height = height0;
    for (int i = 0; i < width * height; i++)
      mat.emplace_back(rand() % 255);
  }
  unsigned char &operator()(int row, int col) { return mat[row * width + col]; }
  unsigned char operator()(int row, int col) const {
    // do not use at here, the extra check is too expensive for the tight loop
    return mat[row * width + col];
  }
};

std::vector<float> __attribute__((noinline)) average(const Matrix &original) {
  std::vector<float> average(original.width, 0.0);
  // We can't do array reduction directly on vectors
  auto average_data = average.data();

  #pragma omp parallel reduction(+ : average_data[ : original.width])
  {
    #pragma omp for
    for (int row = 0; row < original.height; row++) {
      for (int col = 0; col < original.width; col++) {
        average_data[col] += original(row, col);
      }
    }
  }
  for (float &val : average) {
    val /= original.height;
  }
  return average;
}

int main() {
  Matrix mat(500, 20000);

  std::cerr << mat.width << " " << mat.height << std::endl;

  std::chrono::steady_clock::time_point begin = chrono::steady_clock::now();
  double dont_optimize = 0;
  for (int i = 0; i < 100; i++) {
    auto ret = average(mat);
    dont_optimize += ret[0];
  }
  std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();

  std::cout << "Time = "
            << (std::chrono::duration_cast<std::chrono::microseconds>(end-begin).count() / 100.)
            << "\n" << optimize << std::endl;
  return 0;
}
对于给定的矩阵大小,在2.5 GHz nomina的Intel Xeon E5-2680 v3上使用12个线程,这将时间从~1.8毫秒缩短到~0.3毫秒 l频率

切换循环 或者,您可以并行化内部循环,因为它的迭代彼此独立。但是,由于每个线程的工作量很小,因此速度会较慢。然后可以交换内部循环和外部循环,但这会导致内存访问不连续,这对性能也不利。因此,这种方法的最佳方法是像这样拆分内部循环:

constexpr size_t chunksize = 128;
#pragma omp parallel for
for (size_t col_chunk = 0; col_chunk < original.width; col_chunk += chunksize) {
  for (size_t row = 0; row < original.height; row++) {
    const auto col_end = std::min(col_chunk + chunksize, original.width);
    for (size_t col = col_chunk; col < col_end; col++) {

这为您提供了合理的连续内存访问,同时避免了线程之间的所有交互。但是,在线程的边界上仍然可能存在一些错误共享。我一直无法轻松获得很好的性能,但它仍然比具有足够线程数的串行更快。

坦率地说,这看起来更像是SIMD的工作,而不是线程。您的优化器的循环向量器可能已经做得很好了。用线程打败它会很困难。在涉及线程之前,我要看的另一件事是将值累加为整数,并在最后的除法步骤中只将它们提升一倍。你使用的矩阵大小是多少?@Zulan我一直在用大约20000列和500行的矩阵做实验。哇,非常感谢您如此详细的解释!我原以为我会轻松地获得很多成绩,但显然不是。我将尝试运行一些实验,看看我是否能从巨大的矩阵中获得收益。这个答案的结论严重偏向于对微小矩阵的假设。@Zulan:结论的一部分。无论矩阵大小如何,平面阵列索引都会更快。不管怎样,我现在对10000x1000矩阵进行测试,并发现相同的结果:我假设这是由于原子的同步效应。欢迎你把这个尺寸放大,看看你是否发现了一些不同的东西。我发布了一个补充答案,解释了如何用线程来加速这个过程。我很确定您的性能问题源于在Mat::operator实现中使用运行时checked.at而不是operator[]。通常情况下,编译器会为具有这种访问权限的循环生成合理的代码,您不必手动执行连续漫游。@Zulan:使用运算符[]而不是。at确实提高了速度;我相应地修改了时间。我认为编译器仍然在生成次优代码,因为他们使用的是VADDS而不是vaddps。这很好-谢谢。从这里开始,唯一可以做的就是让AVX正常工作。
constexpr size_t chunksize = 128;
#pragma omp parallel for
for (size_t col_chunk = 0; col_chunk < original.width; col_chunk += chunksize) {
  for (size_t row = 0; row < original.height; row++) {
    const auto col_end = std::min(col_chunk + chunksize, original.width);
    for (size_t col = col_chunk; col < col_end; col++) {