OpenMP+MPI混合梯形积分程序:1/2进程多线程无性能提升问题
问题:OpenMP+MPI混合并行积分程序的性能差异原因分析
问题描述
我在Linux环境下运行一款基于OpenMP+MPI混合并行的梯形法积分程序,计算区间[-1,1]上1亿个区间的积分值。测试发现:当进程数为1或2时,调整每个进程的线程数,程序运行时间几乎无明显变化;但进程数大于2时,增加线程数能显著缩短运行时间。请问该现象的原因是什么?
程序逻辑说明
主进程将1亿个积分区间分片分配给各MPI进程,每个进程通过OpenMP归约操作计算局部积分和,最终由主进程汇总所有局部和得到全局积分结果。
测试数据
| 进程数 | 线程数 | 单进程区间数 | 测试1耗时[s] | 测试2耗时[s] | 测试3耗时[s] | 平均耗时[秒] |
|---|---|---|---|---|---|---|
| 1 | 1 | 100000000 | 79.458205 | 79.849332 | 79.784676 | 79.69740433 |
| 1 | 2 | 100000000 | 79.659298 | 79.555543 | 80.400979 | 79.87194 |
| 1 | 3 | 100000000 | 79.559082 | 79.537585 | 79.535942 | 79.544203 |
| 1 | 4 | 100000000 | 79.594008 | 79.583018 | 80.673965 | 79.95033033 |
| 2 | 1 | 50000000 | 42.891658 | 42.379804 | 42.370149 | 42.54720367 |
| 2 | 2 | 50000000 | 42.447902 | 42.487963 | 42.405998 | 42.44728767 |
| 2 | 3 | 50000000 | 42.436457 | 42.454243 | 42.431626 | 42.44077533 |
| 2 | 4 | 50000000 | 42.58225 | 42.536839 | 42.526669 | 42.548586 |
| 3 | 1 | 33333333 | 28.78676 | 34.490129 | 32.66588 | 31.980923 |
| 3 | 2 | 33333333 | 17.099719 | 17.314803 | 16.132476 | 16.84899933 |
| 3 | 3 | 33333333 | 11.878988 | 11.475592 | 11.678058 | 11.677546 |
| 3 | 4 | 33333333 | 9.27199 | 9.091445 | 8.958381 | 9.107272 |
| 4 | 1 | 25000000 | 23.395599 | 25.80253 | 22.365489 | 23.85453933 |
| 4 | 2 | 25000000 | 13.048262 | 12.91933 | 12.891229 | 12.95294033 |
| 4 | 3 | 25000000 | 9.064296 | 9.14309 | 9.300431 | 9.169272 |
| 4 | 4 | 25000000 | 7.121319 | 7.088155 | 7.060605 | 7.090026333 |
编译运行命令(以4个MPI进程、8个OpenMP线程为例)
mpicc -fopenmp -o example example.c -lm mpirun -np 4 ./example 8
程序代码
#include <stdio.h> #include <math.h> #include <stdlib.h> #include <omp.h> #include "mpi.h" long double function(long double x) { return 0.1 * pow(x, 50) - 0.2 * pow(x, 48) + 0.3 * pow(x, 47) - 0.4 * pow(x, 45) + 0.5 * pow(x, 43) - 0.1 * pow(x, 42) + 0.2 * pow(x, 40) - 0.3 * pow(x, 38) + 0.4 * pow(x, 37) - 0.5 * pow(x, 35) + 0.3 * pow(x, 33) - 0.2 * pow(x, 32) + 0.1 * pow(x, 30) - 0.2 * pow(x, 28) + 0.3 * pow(x, 27) - 0.1 * pow(x, 25) + 0.2 * pow(x, 23) - 0.3 * pow(x, 22) + 0.1 * pow(x, 20) + log(pow(x, 2) + 1) + 1; } long double trapezoidal_method(long double local_a, long double local_b, int num_intervals, int num_threads) { omp_set_num_threads(num_threads); long double h = (local_b - local_a) / (double)num_intervals; long double total_sum = 0; long double global_b = local_b; #pragma omp parallel shared(total_sum) { #pragma omp for reduction(+:total_sum) for (int i = 1; i < num_intervals; i++) { long double a = local_a + (i - 1) * h; long double b = local_a + i * h; total_sum += 0.5 * (function(a) + function(b)) * h; } } //printf("%Lf \n",total_sum); return total_sum; } int main(int argc, char *argv[]) { int rank, size; MPI_Status status; MPI_Init(&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD, &rank); MPI_Comm_size(MPI_COMM_WORLD, &size); long double partial_sum, global_sum; long double parameters[3]; double start_time, end_time; long double interval_start = -1; long double interval_end = 1; int num_intervals = 100000000; int num_threads = atoi(argv[1]); int intervals_per_process = (int) floor(num_intervals / size); long double step_per_process = (interval_end - interval_start) / size; start_time = omp_get_wtime(); if (rank == 0) { partial_sum = 0; long double a = interval_start; long double b = interval_start + step_per_process; int intervals_left = num_intervals; for (int i = 1; i < size; i++) { long double parameters[3] = {a, b, intervals_per_process}; MPI_Send(parameters, 3, MPI_LONG_DOUBLE , i, 0, MPI_COMM_WORLD); a = b; b += step_per_process; intervals_left -= intervals_per_process; } partial_sum = trapezoidal_method(a, interval_end, intervals_left, num_threads); } else { MPI_Recv(¶meters, 3, MPI_LONG_DOUBLE, 0, 0, MPI_COMM_WORLD, &status); partial_sum = trapezoidal_method(parameters[0], parameters[1], parameters[2], num_threads); } MPI_Reduce(&partial_sum, &global_sum, 1, MPI_LONG_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD); end_time = omp_get_wtime(); if (rank == 0) { printf("Processes: %d | Threads: %d | Intervals per process: %d | Sum: %Lf | Execution time: %f s. \n", size, num_threads, intervals_per_process, global_sum, end_time - start_time); } MPI_Finalize(); return 0; }
原因分析
1. CPU核心资源的饱和状态
从测试数据的性能拐点推断,你的运行环境应该是一台配备4个物理计算核心的服务器:
- 当进程数为1时,单个MPI进程会占用所有4个核心,此时开启多线程只会导致线程在核心间频繁切换,无法获得额外计算资源,因此耗时几乎不变。
- 当进程数为2时,每个MPI进程分配到2个核心,1线程就已经占满对应核心的计算能力,增加线程同样会引发上下文切换,无法提升效率。
- 当进程数大于2时,比如3个MPI进程,每个进程默认仅占用1个核心,此时给每个进程增加线程,能充分利用剩余的空闲核心(或超线程资源),让更多计算单元参与任务处理,因此耗时显著降低。
2. 任务粒度与并行资源的匹配度
- 进程数1或2时,单个MPI进程的任务量分别为1亿、5000万区间,单线程就能把对应核心的计算能力拉满,多线程拆分任务后没有额外核心可以利用,自然无法缩短耗时。
- 进程数大于2时,单个进程的任务量降至3333万或2500万区间,单线程无法占满核心资源,此时增加OpenMP线程能让更多核心参与计算,充分发挥并行优势。
3. MPI与OpenMP的资源调度逻辑
默认情况下,MPI会将进程绑定到不同的物理核心:
- 进程数≤核心数时,每个MPI进程独占核心,OpenMP线程只能在该核心的超线程队列中运行,无法获得额外的计算资源,性能提升不明显。
- 进程数>核心数时,MPI进程会共享核心资源,此时OpenMP多线程可以调度到空闲的核心上,真正实现多线程并行计算,因此耗时大幅下降。
内容的提问来源于stack exchange,提问作者mzkaoq
相关产品推荐
相关产品推荐

