当另一个进程 运行 时,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 线程的方式有关。
- 如果我将 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 线程。因为您的 运行ning 软件线程比 CPU 线程多,所以一个线程不会 运行ning,导致所有其他线程忙等待,直到内核调度程序重新分配 CPU。
如果您使用 OMP_WAIT_POLICY=passive
调用您的程序,GCC 将设置 GOMP_SPINCOUNT = '0'
。然后内核会立即让等待的线程进入睡眠状态,让其他线程进入运行。现在你的表现会好很多。
有趣的是 OMP_PROC_BIND=true
也有帮助。我假设固定线程会以某种方式影响内核调度程序,这对我们有好处,但我不确定。
Clang 的 OpenMP 实现不会受到这种性能下降的影响,因为它使用 pthread_yield
。当然,如果系统调用开销很大并且在大多数计算环境中这有其自身的缺点,那么它应该是不必要的,因为您不应该过度使用 CPUs.
当尝试在 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 线程的方式有关。
- 如果我将 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 线程。因为您的 运行ning 软件线程比 CPU 线程多,所以一个线程不会 运行ning,导致所有其他线程忙等待,直到内核调度程序重新分配 CPU。
如果您使用 OMP_WAIT_POLICY=passive
调用您的程序,GCC 将设置 GOMP_SPINCOUNT = '0'
。然后内核会立即让等待的线程进入睡眠状态,让其他线程进入运行。现在你的表现会好很多。
有趣的是 OMP_PROC_BIND=true
也有帮助。我假设固定线程会以某种方式影响内核调度程序,这对我们有好处,但我不确定。
Clang 的 OpenMP 实现不会受到这种性能下降的影响,因为它使用 pthread_yield
。当然,如果系统调用开销很大并且在大多数计算环境中这有其自身的缺点,那么它应该是不必要的,因为您不应该过度使用 CPUs.