如何优化并行嵌套循环?

Posted

技术标签:

【中文标题】如何优化并行嵌套循环?【英文标题】: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。现已更正。 我已经更新了代码并尝试展开 【参考方案1】:

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

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

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

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

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

    不使用 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。为你的循环计时,以确保那些确实能提高你的速度。

【讨论】:

以上是关于如何优化并行嵌套循环?的主要内容,如果未能解决你的问题,请参考以下文章

在 OpenMP 中并行化嵌套循环并使用更多线程执行内部循环

CUDA:并行化具有嵌套循环的函数调用的多个嵌套for循环

在R中将嵌套的for循环转换为并行

openMP 嵌套并行 for 循环与内部并行 for

如何优化不同长度的嵌套for循环?

在java中,如何跳出当前的嵌套循环