当另一个进程 运行 时,OpenMP 非常慢

OpenMP incredibly slow when another process is running

当尝试在 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 循环内,因为我的实际代码是在相互依赖的时间步上循环。

我的设置

我 运行 Ubuntu 21.04 上的示例使用 AMD Ryzen 9 5900X(12 核,24 线程),并使用 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 allocates/assigns 线程的方式有关。

从这些发现来看,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 线程。因为您的 运行ning 软件线程比 CPU 线程多,所以一个线程不会 运行ning,导致所有其他线程忙等待,直到内核调度程序重新分配 CPU。

如果您使用 OMP_WAIT_POLICY=passive 调用您的程序,GCC 将设置 GOMP_SPINCOUNT = '0'。然后内核会立即让等待的线程进入睡眠状态,让其他线程进入运行。现在你的表现会好很多。

有趣的是 OMP_PROC_BIND=true 也有帮助。我假设固定线程会以某种方式影响内核调度程序,这对我们有好处,但我不确定。

Clang 的 OpenMP 实现不会受到这种性能下降的影响,因为它使用 pthread_yield。当然,如果系统调用开销很大并且在大多数计算环境中这有其自身的缺点,那么它应该是不必要的,因为您不应该过度使用 CPUs.