虽然这篇文章有点过时,但我仍然想为此提供一些有用的见解。
从功能的角度来看,上述答案是正确的,但从性能的角度来看不会给出最佳结果。原因是大多数 OpenMP 实现在线程到达障碍或没有工作要做时不会关闭线程。相反,线程将进入自旋等待循环并在等待时继续消耗处理器周期。
在示例中:
#pragma omp parallel
{
#omp for nowait
for(...) {} // first loop
#omp for
for(...) {} // second loop
#pragma omp single
dgemm_(....)
#pragma omp for
for(...) {} // third loop
}
将会发生的情况是,即使dgemm
调用在 MKL 中创建了额外的线程,外层线程仍将积极等待single
构造结束,因此dgemm
会以降低的性能运行。
这个问题基本上有两种解决方案:
1)列表项使用上面的代码,除了建议的环境变量外,还禁用主动等待:
$ MKL_DYNAMIC=FALSE MKL_NUM_THREADS=8 OMP_NUM_THREADS=8 OMP_NESTED=TRUE OMP_WAIT_MODE=passive ./exe
2)修改代码分割并行区域:
#pragma omp parallel
{
#omp for nowait
for(...) {} // first loop
#omp for nowait
for(...) {} // second loop
}
dgemm_(...);
#pragma omp parallel
#pragma omp for nowait
for(...) {} // third loop
}
对于解决方案 1,线程立即进入睡眠模式并且不消耗周期。缺点是线程必须从这种更深的睡眠状态中唤醒,与自旋等待相比,这会增加延迟。
对于解决方案 2,线程保持在其自旋等待循环中,并且很可能在dgemm
调用进入其并行区域时主动等待。额外的连接和分叉也会引入一些开销,但它可能比使用single
构造或解决方案 1 的初始解决方案的超额订阅要好。
最好的解决方案取决于dgemm
操作中完成的工作量与 fork/join 的同步开销相比,后者主要由线程数和内部实现主导。