【问题标题】:How to optimally parallelize nested loops?如何优化并行嵌套循环?
【发布时间】:2016-11-19 10:06:23
【问题描述】:

我正在编写一个应该在串行和并行版本中运行的程序。一旦我让它真正完成它应该做的事情,我就开始尝试将它与 OpenMP 并行化(强制)。

问题是我找不到关于何时使用 #pragma 的文档或参考资料。所以我正在尽力猜测和测试。但是使用嵌套循环进行测试并不顺利。

您将如何并行化一系列嵌套循环,例如:

for(int i = 0; i < 3; ++i){
    for(int j = 0; j < HEIGHT; ++j){
        for(int k = 0; k < WIDTH; ++k){
            switch(i){
                case 0:
                        matrix[j][k].a = matrix[j][k] * someValue1;
                        break;
                case 1:
                        matrix[j][k].b = matrix[j][k] * someValue2;
                        break;   
                case 2:
                        matrix[j][k].c = matrix[j][k] * someValue3;                
                        break;
            }
        }
    }
}
  • 在我必须运行的测试中,HEIGHT 和 WIDTH 通常大小相同。一些测试示例是 32x32 和 4096x4096。
  • matrix 是一组具有属性 a、b 和 c 的自定义结构
  • someValue 是一个双精度值

我知道 OpenMP 并不总是适用于嵌套循环,但欢迎提供任何帮助。

[更新]:

到目前为止,我已经尝试展开循环。它提高了性能,但我在这里增加了不必要的开销吗?我在重用线程吗?我尝试获取每个 for 中使用的线程的 ID,但没有正确。

#pragma omp parallel
        {
#pragma omp for collapse(2)
            for (int j = 0; j < HEIGHT; ++j) {
                for (int k = 0; k < WIDTH; ++k) {
                    //my previous code here
                }
            }
#pragma omp for collapse(2)
            for (int j = 0; j < HEIGHT; ++j) {
                for (int k = 0; k < WIDTH; ++k) {
                    //my previous code here
                }
            }
#pragma omp for collapse(2)
            for (int j = 0; j < HEIGHT; ++j) {
                for (int k = 0; k < WIDTH; ++k) {
                    //my previous code here
                }
            }
        }

[更新 2]

除了展开循环之外,我还尝试并行化外循环(比展开最差的性能提升)并折叠两个内循环(与展开或多或少相同的性能提升)。这是我得到的时间。

  • 串行:~130 毫秒
  • 循环展开:~49 毫秒
  • 折叠两个最里面的循环:~55 ms
  • 并行最外层循环:~83 ms

您认为最安全的选择是什么?我的意思是,对于大多数系统来说,哪个应该是最好的,而不仅仅是我的电脑?

【问题讨论】:

  • 抱歉打错了。现在更正@HighPerformanceMark
  • 我认为最内层循环中的ik 的拼写错误?
  • 是的,@Davislor。现已更正。
  • 我已经更新了代码并尝试展开

标签: c++ parallel-processing openmp


【解决方案1】:

OpenMP 的问题在于它非常高级,这意味着您无法访问低级功能,例如生成线程,然后重用它。所以让我说清楚你能做什么,不能做什么:

假设您不需要任何互斥锁来防止race conditions,您可以选择以下选项:

  1. 您将最外层循环并行化,这将使用 3 个线程,这是您将拥有的最和平的解决方案

  2. 将第一个内部循环与执行最内层循环。

  3. 并行化最内部的循环,但这是世界上最糟糕的解决方案,因为您将重新生成线程 3*HEIGHT 次。永远不要那样做!

  4. 不使用 OpenMP,而使用低级别的东西,例如 std::thread,您可以在其中创建自己的线程池,并将您想要执行的所有操作推送到队列中。

希望这有助于正确看待事情。

【讨论】:

  • 如果我发布一些示例HEIGHTWIDTH 会更好吗?当您说并行化某些循环时,您的意思是仅使用 #pragma omp parallel for 而没有任何 collapse(n) 或其他子句,对吗?您是否考虑过折叠这些循环中的任何一个?
  • 好的 OpenMP 库确实使用线程池并重复使用它们。他们不会每次都启动一个新线程。当然,同步仍然有很多开销。在这里崩溃将是一件好事。
  • @danielsto 使用collapse 是个好主意,如果Vladimir 是对的,那么如果在OpenMP 中自动使用线程池,你会很幸运,但这不是我使用它的经验。不幸的是,一个示例将无济于事,因为这非常依赖于您的系统。您所能做的就是逐案计划、尝试和研究。
  • @VladimirF 其实想想,如果一个库确实使用了线程池,那么collapse也没用。它根本不会提高性能。对吗?
  • @danielsto 计算中的一条经验法则是:代码越通用,获得的性能就越低。例如,BLAS 是著名的矩阵乘法 API。如果您创建一个实现来在任何地方运行它,那么它不是最好的。 OpenBLAS 根据你的处理器创建一个实现,这是你能得到的最好的。你明白重点了吗?所以你必须更通用,牺牲性能,但要遵循不依赖于系统的一般指导方针。尽量减少线程产生的数量,你会没事的。我猜这是你能做的最好的了。
【解决方案2】:

这是另一种选择,它认识到在只有 3 个循环迭代时分配最外层循环的迭代可能会导致负载平衡非常差,

i=0
#pragma omp parallel for
for(int j = 0; j < HEIGHT; ++j){
    for(int k = 0; k < WIDTH; ++k){
    ...
}

i=1
#pragma omp parallel for
for(int j = 0; j < HEIGHT; ++j){
    for(int k = 0; k < WIDTH; ++k){
    ...
}

i=2
#pragma omp parallel for
for(int j = 0; j < HEIGHT; ++j){
    for(int k = 0; k < WIDTH; ++k){
    ...
}

警告——自己检查语法,这不过是手动展开循环的草图。

尝试将其组合并折叠 jk 循环。

哦,不要抱怨代码重复,您已经告诉我们,您的部分得分来自性能改进。

【讨论】:

  • 这个和把这个放在一个循环中循环i有什么区别?我不明白。
  • 不确定我是否理解。你的意思是说离开最外层的循环可能会导致负载平衡不佳吗?所以展开循环会带来更好的负载平衡,对吧?
【解决方案3】:

您可能希望并行化此示例for simd,以便编译器可以向量化collapse 循环,因为您仅在表达式matrix[j][k] 中使用jk,并且因为没有任何依赖关系矩阵的其他元素。如果没有修改somevalue1 等,它们应该是uniform。为你的循环计时,以确保那些确实能提高你的速度。

【讨论】:

    猜你喜欢
    • 2020-09-27
    • 2021-01-21
    • 2018-03-09
    • 1970-01-01
    • 1970-01-01
    • 2018-09-16
    • 2012-01-27
    相关资源
    最近更新 更多