如何优化并行嵌套循环?
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 我认为最内层循环中的i
是k
的拼写错误?
是的,@Davislor。现已更正。
我已经更新了代码并尝试展开
【参考方案1】:
OpenMP 的问题在于它非常高级,这意味着您无法访问低级功能,例如生成线程,然后重用它。所以让我说清楚你能做什么,不能做什么:
假设您不需要任何互斥锁来防止race conditions,您可以选择以下选项:
您将最外层循环并行化,这将使用 3 个线程,这是您将拥有的最和平的解决方案
将第一个内部循环与执行最内层循环。
并行化最内部的循环,但这是世界上最糟糕的解决方案,因为您将重新生成线程 3*HEIGHT 次。永远不要那样做!
不使用 OpenMP,而使用低级别的东西,例如 std::thread
,您可以在其中创建自己的线程池,并将您想要执行的所有操作推送到队列中。
希望这有助于正确看待事情。
【讨论】:
如果我发布一些示例HEIGHT
和WIDTH
会更好吗?当您说并行化某些循环时,您的意思是仅使用 #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)
...
警告——自己检查语法,这不过是手动展开循环的草图。
尝试将其组合并折叠 j
和 k
循环。
哦,不要抱怨代码重复,您已经告诉我们,您的部分得分来自性能改进。
【讨论】:
这个和把这个放在一个循环中循环i
有什么区别?我不明白。
不确定我是否理解。你的意思是说离开最外层的循环可能会导致负载平衡不佳吗?所以展开循环会带来更好的负载平衡,对吧?【参考方案3】:
您可能希望并行化此示例for simd
,以便编译器可以向量化collapse
循环,因为您仅在表达式matrix[j][k]
中使用j
和k
,并且因为没有任何依赖关系矩阵的其他元素。如果没有修改somevalue1
等,它们应该是uniform
。为你的循环计时,以确保那些确实能提高你的速度。
【讨论】:
以上是关于如何优化并行嵌套循环?的主要内容,如果未能解决你的问题,请参考以下文章