如何提高 OpenMP 代码的性能?

Posted

技术标签:

【中文标题】如何提高 OpenMP 代码的性能?【英文标题】:How can I improve the perfomance of my OpenMP code? 【发布时间】:2016-09-28 15:06:16 【问题描述】:

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

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

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

我也不确定日程安排。我是否必须指定是否需要 staticdynamic 时间表?我可以使用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

【问题讨论】:

作为第一步,始终找出瓶颈,即...(留作练习) 你确定这段代码是可并行的吗?如果并行执行代码,则存在无法满足的依赖关系。 因为我只使用前一个时间步长 (data) 中的数据点来计算新时间步长 newData 中的数据点,所以我看不到任何破坏并行化本身的依赖项。 乍一看,代码看起来不错。只需使用schedule(static) 并摆脱critical。并确保在启用最大优化的情况下进行编译。 所以critical 不是必需的吗?我不确定。谢谢 【参考方案1】:

首先,摆脱所有不必要的依赖。 #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;

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

慢除法 缓存抖动 ILP(指令级并行) 内存带宽限制 隐藏的依赖关系

让我们进一步分析它们。

慢速划分: 计算 double/double 需要一些 CPU 永远。要知道您的 CPU 的运行时间和吞吐量,您必须查看它的规格。也许用*0.3333.. 替换/3.0 可能会有所帮助,但也许你的编译器已经这样做了。使用扩展指令集(如 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 个(*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.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。我想这其中的原因你自己会发现的。

享受优化的乐趣;-)

【讨论】:

【参考方案2】:
    多线程读取数组通常没有害处。 如果多个线程处理完全相同的数据块,您只需要一个临界区,这里每个线程访问数组的不同部分,因此您不需要它。关键部分对性能非常不利,因此仅在绝对必要时才使用它们。通常它们可以被原子操作替换: openMP, atomic vs critical? 就像临界区一样,如果每个线程访问不同的数据,它们就没有意义。 对于调度程序,最好对它们进行测试并测量性能,因为对性能的预测通常是错误的。还可以尝试不同的块大小。 其他一些可能有帮助的事情: 测量性能通常会受到电脑上其他任务的干扰,因此请进行多次测量并取其最小值(除非每次输入不同,然后取平均值并进行更多测量)。 您真的需要双精度吗?浮点数要快得多。 编辑:nowait 用于 多个 独立的 for 循环:https://msdn.microsoft.com/en-us/library/ek5st0e3.aspx

【讨论】:

【参考方案3】:

我假设您正在尝试使用一维数组进行某种卷积或中值模糊。简短的回答是:坚持默认的计划策略,完全摆脱关键。

据我所知,您是并行处理的新手,处理 OpenMP 指令有点混乱,例如 nowait/private/reduction/critical/atomic/single 等。我认为您需要的是一个井编写教科书以阐明各种概念。如果您有扎实的知识,学习 OpenMP 一个小时就足以应付大多数日常编程。

【讨论】:

以上是关于如何提高 OpenMP 代码的性能?的主要内容,如果未能解决你的问题,请参考以下文章

什么可以阻止多处理提高速度 - OpenMP?

openmp的性能

openmp:线程数的增加会降低性能

为啥 OpenMP 'simd' 比 'parallel for simd' 有更好的性能?

fread OpenMP 线程中的性能下降

如何编写在 C++ openmp 上重用线程的代码?