c++ - 如何提高我的 OpenMP 代码的性能?

标签 c++ multithreading performance openmp

我目前正在尝试提高我的代码的并行性能,但我对 OpenMP 还是个新手。我必须遍历一个大容器,在每次迭代中读取多个条目并将结果写入单个条目。下面是我正在尝试做的一个非常简单的代码示例。

data 是一个指向数组的指针,数组中存储了很多数据点。在并行区域之前,我创建了一个数组 newData,因此可以将 data 用作只读,将 newData 用作只写,之后我抛出旧的 data 并使用 newData 进行进一步的计算。 据我了解,datanewData 在线程之间共享,并且在并行区域内声明的所有内容都是私有(private)的。 多线程读取数据会导致性能问题吗?

我正在使用 #criticalnewData 的元素分配新值以避免竞争条件。这是必要的吗,因为我只访问了 newData 的每个元素一次,而且从不通过多个线程访问?

我也不确定时间安排。我是否必须指定是要static 还是dynamic 时间表?我可以使用 nowait 因为所有线程都是相互独立的吗?

array *newData = new array;

omp_set_num_threads (threads);

#pragma omp parallel
{
    #pragma omp for
    for (int i = 0;  i < range; i++)
    {
        double middle = (*data)[i];
        double previous = (*data)[i-1];
        double next = (*data)[i+1];

        double new_value = (previous + middle + next) / 3.0;
        #pragma omp critical(assignment)
        (*newData)[i] = new_value;
    }
}

delete data;
data = newData;

我知道在第一次和最后一次迭代中 previousnext 不能从 data 中读取,在真实代码中这是已处理,但对于这个最小的示例,您会想到从 data 中多次读取。

最佳答案

首先,摆脱所有不必要的依赖。 #pragma omp critical(assignment) 不是必需的,因为 (*newData) 的每个索引在每个循环中只写入一次,因此不存在竞争条件。

你的代码现在看起来像这样:

#pragma omp parallel for
for (int i = 0; i < range; i++)
   (*newData)[i] = ((*data)[i-1] + (*data)[i] + (*data)[i+1]) / 3.0;

现在我们正在寻找瓶颈。我想出的潜在候选人名单是这样的:

  • 慢 split
  • 缓存抖动
  • ILP(指令级并行)
  • 内存带宽限制
  • 隐藏的依赖

那么让我们进一步分析它们。

慢除法: 计算 double/double 需要一些 CPU 永远。要了解您的 CPU 的运行时间和吞吐量,您必须查看其规范。也许将 /3.0 替换为 *0.3333.. 可能会有所帮助,但也许您的编译器已经这样做了。使用扩展指令集(如 SSE/AVX),您可以一次安排多个除法/乘法。

缓存抖动: 因为您的 CPU 必须一次加载/存储一个缓存行,所以可能会发生冲突。想象一下,如果线程 1 尝试写入 (*newdata)[1],而线程 2 尝试写入 (*newdata)[2],并且它们位于同一缓存行上。现在他们中的一个必须等​​待另一个。您可以使用 #pragma omp parallel for schedule(static, 64) 解决此问题。

ILP: 如果操作是独立的,CPU 可以将多个操作安排到管道中。为此,您必须展开循环。这可能看起来像这样:

assert(range % 4 == 0);
#pragma omp parallel for
for (int i = 0; i < range/4; i++) {
   (*newData)[i*4+0] = ((*data)[i*4-1] + (*data)[i*4+0] + (*data)[i*4+1]) / 3.0;
   (*newData)[i*4+1] = ((*data)[i*4+0] + (*data)[i*4+1] + (*data)[i*4+2]) / 3.0;
   (*newData)[i*4+2] = ((*data)[i*4+1] + (*data)[i*4+2] + (*data)[i*4+3]) / 3.0;
   (*newData)[i*4+3] = ((*data)[i*4+2] + (*data)[i*4+3] + (*data)[i*4+4]) / 3.0;
}

内存带宽限制: 对于您非常简单的循环,请考虑一下。您需要加载多少内存以及您的 CPU 将忙于处理它多长时间。您正在加载大约 1 个缓存行并计算一些取消引用、一些指针加法、两次加法和一次除法。您达到的限制取决于您的 CPU 规范。 现在考虑缓存局部性。您可以修改代码以更好地利用缓存吗?如果一个线程在一个循环迭代中获得 i=3,而在下一个循环迭代中获得 i=7,则您必须重新加载 3 个(*数据)。但是如果你从 i=3 到 i=4,你可能不需要加载任何东西,因为 (*data)[i+1] 在之前加载的缓存行中。您节省了一些 RAM 带宽。要利用它,展开循环。使用 float 而不是 double 也会增加这种机会。

隐藏的依赖项: 现在这部分我个人觉得非常棘手。有时您的编译器不确定它是否可以重用某些数据,因为它不知道它没有改变。使用 const 有助于编译器。但有时您需要 restrict 来为编译器提供正确的提示。但我不太了解这一点,无法解释。

所以这是我会尝试的:

const double ONETHIRD = 1.0 / 3.0;
assert(range % 4 == 0);
#pragma omp parallel for schedule(static, 1024)
for (int i = 0; i < range/4; i++) {
   (*newData)[i*4+0] = ((*data)[i*4-1] + (*data)[i*4+0] + (*data)[i*4+1]) * ONETHIRD;
   (*newData)[i*4+1] = ((*data)[i*4+0] + (*data)[i*4+1] + (*data)[i*4+2]) * ONETHIRD;
   (*newData)[i*4+2] = ((*data)[i*4+1] + (*data)[i*4+2] + (*data)[i*4+3]) * ONETHIRD;
   (*newData)[i*4+3] = ((*data)[i*4+2] + (*data)[i*4+3] + (*data)[i*4+4]) * ONETHIRD;
}

然后是基准测试。多做一些基准测试,多做一些基准测试。只有基准测试会告诉您哪些技巧有用。

PS:还有一件事需要考虑。如果您发现您的程序严重占用了内存带宽。您可以考虑更改算法。也许将两步合二为一。就像从 b[i] := (a[i-1] + a[i] + a[i+1])/3.0d[i] := (n[i-1] + n[i] + n[i+1])/3.0 = (a[i-2] + 2.0 * a[i-1] + 3.0 * a[i] + 2.0 * a[i+1] + a[i+1])/3.0。我想您会自己找出原因。

享受优化的乐趣 ;-)

关于c++ - 如何提高我的 OpenMP 代码的性能?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/39751294/

相关文章:

java - 调度程序可以挂起一个线程并执行另一个线程/工作吗?

performance - Android Studio : aapt. exe 创建了太多进程,使 Android Studio 速度很慢

javascript - Github 应该用作 JavaScript 库的 CDN 吗?

c++ - 旧发行版上的二进制兼容性(使用 C++11)

c++ - 如何修复模板内的错误重构 decltype

Javascript将SharedArrayBuffer同步到主线程

c - 测量多线程 C 程序的加速(使用 Pthreads 实现)

c++ - 如何将结构的 C++/CLI 数组编码为非托管 C++

c++ - 从输入文件中读取十六进制列

ruby-on-rails - Ruby on Rails网站速度变慢