如何提高OpenMP代码的性能?
How can I improve the perfomance of my OpenMP code?
我目前正在努力提高我的代码并行性能,我仍然是OpenMP的新手。我必须在一个大容器上迭代,在每次迭代中从多个条目读取并将结果写入单个条目。下面是我试图做的一个非常简单的代码示例。
data
是一个指向数组的指针,其中存储了大量数据点。在并行区域之前,我创建了一个数组newData
,因此可以使用data
作为只读,newData
作为只写,之后我扔掉了旧的data
,并使用newData
进行进一步的计算。根据我的理解,data
和newData
在线程之间共享,并且在并行区域内声明的所有内容都是私有的。从data
读取多个线程会导致性能问题吗?
我使用#critical
为newData
的元素分配一个新值,以避免竞争条件。这是必要的,因为我访问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;
我知道在第一次和最后一次迭代previous
和next
不能从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;
现在我们要寻找瓶颈。我想到的潜在候选人名单是这样的:
- <
- 缓慢部门/gh>
- 缓存抖动
- ILP(指令级并行)
- 内存带宽限制 <
- 隐藏依赖/gh>
让我们进一步分析一下。
缓慢部门:计算double/double需要一些cpu。要了解CPU的运行时间和吞吐量,必须查看其规格。也许用*0.3333..
替换/3.0
可能会有所帮助,但也许您的编译器已经这样做了。使用扩展指令集(如SSE/AVX),您可以一次安排多个除法/乘法。
缓存不足:因为你的CPU必须一次加载/存储一条缓存线,所以可能会有冲突。想象一下,如果线程1试图写(*newdata)[1],线程2试图写(*newdata)[2],它们在同一缓存行上。现在其中一个要等另一个。您可以使用#pragma omp parallel for schedule(static, 64)
来解决这个问题。
: 如果多个操作是独立的,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 (*data)。但是如果要从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.0
来d[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
。我想原因你自己会发现的。
祝你优化得愉快;-)
- 在多个线程中读取一个数组通常是无害的。
- 你只需要一个临界区,如果多个线程工作在完全相同的数据块,这里每个线程访问数组的不同部分,所以你不需要它。临界区对性能非常不利,所以只有在绝对必要时才使用它们。通常它们可以被原子动作取代:openMP,原子还是临界?像临界区一样,如果每个线程访问不同的数据,它们就没有意义了。
- 对于调度器,最好对它们分别进行测试并测量性能,因为对性能的预测通常是错误的。也可以尝试不同的块大小。
- 其他一些可能有帮助的事情:
- 测量性能经常受到pc上其他任务的干扰,因此进行多次测量并取最小值(除非每次输入不同,然后取平均值并进行更多测量)。 你真的需要双精度吗?浮动更快。
- 编辑:nowait是为多个独立的for循环:https://msdn.microsoft.com/en-us/library/ek5st0e3.aspx
我假设你正在尝试用1D数组做某种卷积或中值模糊。简短的回答是:坚持默认的时间表策略,并摆脱关键。
正如我所知,你是并行的新手,处理OpenMP指令有点混乱,比如nowait/private/reduction/critical/atomic/single等。我认为你需要的是一本写得好的教科书来阐明各种概念。如果您有一定的基础知识,那么学习OpenMP一个小时就足以处理大多数日常编程。
- GCC 和 Clang 代码性能的巨大差异
- 如何使用本征提高性能?(包括示例代码)
- 在.cpp文件中定义方法而不是在 C++ 的 .h 文件中定义方法时,如何提高代码的性能?
- 使用 const double* const 作为模板参数 - 代码性能问题
- 如何在没有性能命中的情况下抽象SIMD代码来处理不同的数据类型
- 为什么 C++ 代码实现的性能不比 python 实现更好?
- 如何提高此 OpenCL 缩减内核代码的性能?
- 相同的代码在不同的 gcc 编译器中存在巨大的性能差异
- 如何基准C 代码的性能
- 相同的代码执行两次:性能差异
- 附加到 C++/CLI dll 的性能探查器无法访问本机C++代码
- 牢记干净的代码的性能,什么更好
- 在将其尺寸较大的向量移动到容量较小的向量之前,是否可以通过使用Reserve()来提高代码性能
- 如何提高四叉树代码的性能以防止程序冻结
- C++多线程性能比单线程代码慢
- 在C++代码中使用纯 C 库是否有性能下降/损失
- 如何使用QueryPerformanceCounter测试现有代码的性能
- 公开类的内部组件,以防止编写过多代码和影响性能
- 从编译器优化和代码性能的角度来看,"if constexpr"与"if"
- C++按字符串调用函数,比较PHP的性能,如何在C++中优化代码