当尝试在 C++ 应用程序中使用 OpenMP 时,我遇到了严重的性能问题,其中多线程性能可能比单线程性能差 1000 倍。仅当至少一个核心被另一进程耗尽时才会发生这种情况。
经过一番挖掘,我可以将问题隔离为一个小示例,我希望有人能够阐明这个问题!
最小示例
这是一个说明问题的最小示例:
#include <iostream>
int main() {
int sum = 0;
for (size_t i = 0; i < 1000; i++) {
#pragma omp parallel for reduction(+:sum)
for (size_t j = 0; j < 100; j++) {
sum += i;
}
}
std::cout << "Sum was: " << sum << std::endl;
}
我需要 OpenMP 指令位于外部 for 循环内部,因为我的实际代码是在相互依赖的时间步上循环的。
我的设置
我在使用 AMD Ryzen 9 5900X(12 核、24 线程)的 Ubuntu 21.04 上运行了该示例,并使用 g++ -fopenmp example.cc
使用 G++ 10.3.0 对其进行了编译。
基准测试
如果您在后台没有任何其他内容的情况下运行该程序,它会很快终止:
> time ./a.out
Sum was: 999000
real 0m0,006s
user 0m0,098s
sys 0m0,000s
但是,如果另一个进程使用单个核心,它的运行速度会非常慢。在本例中,我运行 stress -c 1
来模拟另一个完全使用后台核心的进程。
> time ./a.out
Sum was: 999000
real 0m8,060s
user 3m2,535s
sys 0m0,076s
速度减慢了 1300 倍。我的机器有 24 个并行线程,因此当其中一个线程繁忙且另外 23 个线程可用时,理论上的速度减慢应该仅为 4% 左右。
调查结果
问题似乎与 OpenMP 如何分配/指派线程有关。
- 如果我将 omp 指令移至外循环,问题就会消失
- 如果我明确将线程数设置为 23,问题就会消失 (
num_threads(23)
) - 如果我明确将线程数设置为 24,问题仍然存在
- 进程终止所需的时间从 1 到 8 秒不等
- 程序在运行时不断使用尽可能多的 CPU,我假设大多数 OpenMP 线程都处于自旋锁中
从这些发现来看,OpenMP 似乎将作业分配给所有核心,包括已经达到极限的核心,然后以某种方式强制每个核心完成其任务,并且不允许在其他核心完成时重新分配它们.
我尝试将计划更改为动态,但这也没有帮助。
如果有任何建议,我都会非常有帮助,我是 OpenMP 新手,所以我可能犯了一个错误。你对此有何看法?
最佳答案
所以这是我能弄清楚的:
使用 OMP_DISPLAY_ENV=verbose
运行程序(有关环境变量列表,请参阅 https://www.openmp.org/spec-html/5.0/openmpch6.html)
详细设置将显示 OMP_WAIT_POLICY = 'PASSIVE'
和 GOMP_SPINCOUNT = '300000'
。换句话说,当一个线程必须等待时,它会自旋一段时间然后进入休眠状态,消耗CPU时间并阻塞一个CPU。每次线程到达循环末尾时或在主线程分发 for 循环之前,甚至可能在并行部分开始之前,都会发生这种情况。
由于 GCC 的 libgomp 不使用 pthread_yield
,这实际上会阻塞一个 CPU 线程。由于正在运行的软件线程多于 CPU 线程,因此其中一个线程不会运行,导致所有其他线程忙等待,直到内核调度程序重新分配 CPU。
如果您使用 OMP_WAIT_POLICY=passive
调用程序,GCC 将设置 GOMP_SPINCOUNT = '0'
。然后内核将立即将等待线程置于 sleep 状态并允许其他线程运行。现在你的表现将会好得多。
有趣的是OMP_PROC_BIND=true
也有帮助。我认为不可移动的线程会以某种方式影响内核调度程序,这对我们有利,但我不确定。
Clang 的 OpenMP 实现不会受到这种性能下降的影响,因为它使用 pthread_yield
。当然,如果系统调用开销很大,并且在大多数计算环境中,这有其自身的缺点,那么它应该是不必要的,因为您不应该过度使用 CPU。
关于c++ - 当另一个进程正在运行时,OpenMP 非常慢,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/70126350/