简介:这份资源是电子科技大学分布式并行计算课程的MPI实验报告合集,面向正在学习并行编程、准备课程实验或希望入门高性能计算的学生与开发者。内容围绕MPI标准展开,涵盖点对点通信、集合通信、进程管理、数据分布、并行算法设计以及性能分析与调优等核心知识点,并涉及集群平台上的程序构建、运行与结果验证、死锁等错误处理思路,适合具备一定C语言与操作系统基础的中高级学习者对照实践。资源以7z压缩包形式提供,整体约902KB,文件数量与类型明细上游暂未提供,需下载后自行查看。目前已有1448人学习下载,说明该实验报告在同类课程资料中具有一定参考热度。读者可借助其中的实验过程与代码思路,理解MPI通信原语的实际用法,掌握将排序、矩阵运算等经典算法并行化的方法,并积累负载平衡与通信开销优化的经验,为后续高性能计算方向的学习打下基础。
1. 电子科技大学分布式并行计算 MPI 实验报告:从一份 .7z 压缩包说起
如果你手里正好有一个名为“电子科技大学分布式并行计算-MPI实验报告.7z”的压缩包,或者你正在为分布式并行计算这门课的实验环节发愁,那这篇笔记就是写给你的。MPI(Message Passing Interface)不是什么新东西,但每次在 Windows 上从零搭环境、写第一个点对点通信程序、再到跑通矩阵乘法的并行版本,总有人会在安装、编译、进程数设置这些环节翻车。这份实验报告通常包含几个递进的实验:环境搭建、点对点通信、集合通信、矩阵并行计算,有的还会涉及性能分析。它解决的核心问题是:让你亲手体会“多进程怎么协作算一道题”,而不是停留在背概念的层面。适合谁看?正在上分布式并行计算课、需要交实验报告的学生,以及想快速在 Windows 或 Linux 上把 MPI 跑起来的开发者。下面我按实际做实验的顺序,把每个环节拆开讲。
2. 环境搭建:Windows 上用 Microsoft MPI 跑通第一个程序
2.1 为什么选 Microsoft MPI 而不是 MPICH
在 Windows 上做 MPI 实验,常见的选择有两个:Microsoft MPI(MS-MPI)和 MPICH。MS-MPI 是微软基于 MPICH 做的 Windows 版本,和 Visual Studio 集成得比较好,安装包直接带mpiexec和mpicc的替代工具,适合在 Windows 环境下快速验证。MPICH 跨平台更通用,但 Windows 下编译配置稍微麻烦一点。如果你的实验报告要求“在 Windows 上完成”,我一般会推荐 MS-MPI,省去折腾编译器的功夫。安装包分两部分:msmpisetup.exe和msmpisdk.msi,前者是运行时,后者是开发所需的头文件和库。装完之后,在命令行里敲mpiexec应该能看到用法提示,说明运行时已经就位。
2.2 安装后的环境变量与验证命令
装完 MS-MPI 后,需要确认几个环境变量是否自动配好。打开 PowerShell 或 CMD,执行:
mpiexec -help如果提示找不到命令,手动把C:\Program Files\Microsoft MPI\Bin加到 PATH 里。接着验证 SDK 是否可用,检查C:\Program Files (x86)\Microsoft SDKs\MPI\Include下有没有mpi.h。写一个最小测试程序:
// hello_mpi.c #include <mpi.h> #include <stdio.h> int main(int argc, char** argv) { MPI_Init(&argc, &argv); // 初始化 MPI 环境 int rank, size; MPI_Comm_rank(MPI_COMM_WORLD, &rank); // 当前进程编号 MPI_Comm_size(MPI_COMM_WORLD, &size); // 总进程数 printf("Hello from process %d of %d\n", rank, size); MPI_Finalize(); // 清理 MPI 环境 return 0; }编译命令(用 MS-MPI 自带的mpicc包装脚本,或者直接用 cl.exe 指定头文件和库路径):
mpicc hello_mpi.c -o hello_mpi.exe运行:
mpiexec -n 4 hello_mpi.exe参数说明:-n 4表示启动 4 个进程,每个进程独立执行同一份代码,通过MPI_Comm_rank拿到自己的编号。如果输出顺序是乱的,那是正常的,因为进程调度不由你控制。看到 4 行不同 rank 的输出,环境就算通了。
2.3 在 Visual Studio 里配置 MPI 项目
很多实验报告要求用 VS 写代码,手动配一下项目属性。新建空项目后,右键项目 → 属性 → C/C++ → 常规 → 附加包含目录,加上$(MSMPI_INC);$(MSMPI_INC)\x64。链接器 → 常规 → 附加库目录,加上$(MSMPI_LIB64)。链接器 → 输入 → 附加依赖项,加上msmpi.lib。这样编译时就能找到mpi.h和链接库。注意平台要选 x64,因为 MS-MPI 的库是 64 位的。配好之后,把上面的 hello 程序粘进去,编译运行,在 VS 里直接 F5 只会启动一个进程,要看多进程效果还是得用mpiexec -n 4 your.exe。
3. 点对点通信:从 MPI_Send 到 MPI_Recv 的完整实验
3.1 点对点通信的原理与选型理由
点对点通信是 MPI 最基础的通信模式:一个进程发,另一个进程收。实验报告里通常要求实现一个“进程 0 发消息,进程 1 收消息”的程序,然后扩展到环形传递或乒乓测试。为什么先做这个?因为集合通信(如广播、规约)底层也是用点对点拼出来的,理解MPI_Send和MPI_Recv的阻塞行为,后面调集合通信才不迷糊。关键参数有四个:缓冲区指针、数据个数、数据类型、目标/源进程号、标签、通信域。标签(tag)用来区分同一对进程之间的不同消息,实验里一般用 0 就行。
3.2 一个可复现的乒乓测试代码
下面这个程序让进程 0 和进程 1 来回发一个整数,测 10 次,最后打印总时间。这是实验报告里常见的“通信开销测量”环节。
// pingpong.c #include <mpi.h> #include <stdio.h> int main(int argc, char** argv) { MPI_Init(&argc, &argv); int rank, size; MPI_Comm_rank(MPI_COMM_WORLD, &rank); MPI_Comm_size(MPI_COMM_WORLD, &size); if (size < 2) { printf("Need at least 2 processes\n"); MPI_Finalize(); return 1; } int ping = 1, pong; int count = 10; double start, end; if (rank == 0) { start = MPI_Wtime(); // 开始计时 for (int i = 0; i < count; i++) { MPI_Send(&ping, 1, MPI_INT, 1, 0, MPI_COMM_WORLD); // 发给进程1 MPI_Recv(&pong, 1, MPI_INT, 1, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE); // 收回来 } end = MPI_Wtime(); printf("Process 0: round-trip time for %d iterations = %f seconds\n", count, end - start); } else if (rank == 1) { for (int i = 0; i < count; i++) { MPI_Recv(&pong, 1, MPI_INT, 0, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE); // 先收 MPI_Send(&pong, 1, MPI_INT, 0, 0, MPI_COMM_WORLD); // 再发回 } } MPI_Finalize(); return 0; }编译运行:
mpicc pingpong.c -o pingpong.exe mpiexec -n 2 pingpong.exe逻辑说明:进程 0 先发后收,进程 1 先收后发,形成乒乓。MPI_Wtime()返回墙上时钟秒数,精度足够测毫秒级通信。参数注意:MPI_STATUS_IGNORE表示不关心消息状态,如果实验要求检查来源或标签,可以传一个MPI_Status结构体进去。如果程序卡死,多半是发送和接收的顺序不匹配,比如两个进程都在等对方先发,这就是经典的死锁。
3.3 死锁排查与阻塞通信的边界
点对点通信最容易翻车的地方就是死锁。现象是程序跑起来不动,CPU 占用低,等半天没输出。原因通常是两个进程都先执行了阻塞式MPI_Send,而缓冲区不够大,发送方等接收方取数据,接收方又在等发送方先发,互相卡住。解决办法有两个:一是调整顺序,让一个进程先收再发;二是用非阻塞通信MPI_Isend和MPI_Irecv,把发送和接收都提交后再MPI_Waitall。实验报告里如果要求分析死锁,可以把错误顺序的代码和修正后的代码都跑一遍,对比输出。另外注意,MPI_Send的阻塞行为取决于消息大小和实现,小消息可能被缓冲直接返回,大消息就会阻塞到接收发生,所以不要靠“小消息不阻塞”来侥幸写代码。
4. 集合通信与矩阵并行:把实验报告里的计算题跑出加速比
4.1 集合通信的典型模式与参数含义
集合通信是 MPI 实验报告的重头戏,常见的有MPI_Bcast(广播)、MPI_Reduce(规约)、MPI_Scatter(散射)、MPI_Gather(收集)。以MPI_Bcast为例,进程 0 把一份数据发给所有进程,参数是缓冲区、个数、类型、根进程号、通信域。MPI_Reduce则是把所有进程的数据按指定操作(如MPI_SUM)归约到根进程。实验里通常要求用这些函数实现一个“求数组最大值”或“计算 PI”的程序。选型理由:集合通信内部做了优化,比手写循环点对点快,而且代码简洁。但要注意,集合通信是阻塞的,所有进程必须调用同一个集合函数,否则会挂起。
4.2 矩阵乘法的并行实现与加速比测量
矩阵乘法是分布式并行计算实验的经典题目。假设 C = A × B,A 是 N×N,B 是 N×N。并行策略:按行划分 A,每个进程负责计算 C 的一部分行。进程 0 用MPI_Bcast把 B 广播给所有进程,然后用MPI_Scatter把 A 的行分给各进程,各进程算完局部结果后用MPI_Gather收集回进程 0。下面是一个简化版的核心代码:
// matmul_mpi.c #include <mpi.h> #include <stdio.h> #include <stdlib.h> #define N 512 int main(int argc, char** argv) { MPI_Init(&argc, &argv); int rank, size; MPI_Comm_rank(MPI_COMM_WORLD, &rank); MPI_Comm_size(MPI_COMM_WORLD, &size); int rows = N / size; // 每个进程负责的行数 double *A = NULL, *B = malloc(N * N * sizeof(double)); double *C = NULL, *local_A = malloc(rows * N * sizeof(double)); double *local_C = malloc(rows * N * sizeof(double)); if (rank == 0) { A = malloc(N * N * sizeof(double)); C = malloc(N * N * sizeof(double)); // 初始化 A 和 B(省略具体赋值) } MPI_Bcast(B, N * N, MPI_DOUBLE, 0, MPI_COMM_WORLD); // 广播 B MPI_Scatter(A, rows * N, MPI_DOUBLE, local_A, rows * N, MPI_DOUBLE, 0, MPI_COMM_WORLD); // 分发 A // 局部矩阵乘法 for (int i = 0; i < rows; i++) { for (int j = 0; j < N; j++) { local_C[i * N + j] = 0; for (int k = 0; k < N; k++) { local_C[i * N + j] += local_A[i * N + k] * B[k * N + j]; } } } MPI_Gather(local_C, rows * N, MPI_DOUBLE, C, rows * N, MPI_DOUBLE, 0, MPI_COMM_WORLD); // 收集结果 if (rank == 0) { // 输出或验证 C free(A); free(C); } free(B); free(local_A); free(local_C); MPI_Finalize(); return 0; }编译运行:
mpicc matmul_mpi.c -o matmul_mpi.exe -O2 mpiexec -n 4 matmul_mpi.exe参数说明:N取 512 或 1024,太小了通信开销占比高,加速比不明显。rows = N / size要求 N 能被进程数整除,否则要处理余数,实验报告里可以选 N 为 2 的幂来避开。MPI_Scatter的发送缓冲区只在根进程有效,其他进程传 NULL 也行,但为了代码统一,我一般让所有进程都分配。测量加速比时,分别用 1、2、4 个进程跑,记录时间,算S = T1 / Tp。如果加速比远低于进程数,检查是不是通信占了大头,或者负载不均。
4.3 性能分析:通信开销与计算开销的分离
实验报告通常要求分析并行效率。一个实用技巧:在代码里用MPI_Wtime()分别记录通信和计算的时间。比如在MPI_Bcast前后打时间戳,在矩阵乘法循环前后打时间戳,最后在进程 0 汇总。这样能看出是通信拖了后腿还是计算本身太慢。如果通信时间占比超过 30%,可以考虑减少广播的数据量,或者用非阻塞通信重叠计算和通信。另一个坑是进程数超过物理核心数,比如 4 核机器跑 8 个进程,上下文切换会让加速比反而下降。实验报告里如果要求“分析可扩展性”,可以画一张进程数-加速比的折线图,通常会在某个点后走平甚至下降。
5. 避坑与常见问题:MPI 实验里那些让人抓狂的瞬间
5.1 现象:mpiexec 启动失败,提示“无法启动进程”
原因:MS-MPI 的运行时和 SDK 版本不匹配,或者 PATH 里混入了其他 MPI 实现(比如之前装过 MPICH)。解决:卸载所有 MPI 相关程序,重新安装同一版本的msmpisetup.exe和msmpisdk.msi,确保mpiexec来自C:\Program Files\Microsoft MPI\Bin。在命令行用where mpiexec确认路径唯一。
5.2 现象:程序输出乱序或缺少某些进程的输出
原因:多个进程同时往标准输出写,输出缓冲区竞争。解决:让每个进程把结果写到独立文件,或者用MPI_Barrier同步后再由进程 0 统一打印。实验报告里如果要求“输出有序”,可以在printf前加MPI_Barrier(MPI_COMM_WORLD),但注意这会影响性能测量。
5.3 现象:MPI_Recv 一直阻塞,程序不结束
原因:发送方和接收方的标签、数据类型或进程号不匹配。比如发送用了MPI_INT,接收用了MPI_FLOAT,或者标签一个用 0 一个用 1。解决:检查MPI_Send和MPI_Recv的四个关键参数是否完全一致。可以用MPI_Probe先探测消息再接收,避免盲目等待。
5.4 现象:矩阵乘法结果不对,部分行是 0
原因:MPI_Scatter分发数据时,发送缓冲区在非根进程是无效的,如果非根进程也传了未初始化的指针,可能读到垃圾数据。解决:确保根进程的 A 已经正确初始化,非根进程的A可以传 NULL,但local_A必须分配足够空间。另外检查rows * N的计算是否溢出,N 大时用size_t。
5.5 现象:加速比小于 1,并行反而更慢
原因:进程数太多导致通信开销剧增,或者问题规模太小,计算时间被通信淹没。解决:增大 N,让每个进程的计算量足够大;减少不必要的集合通信;用-n指定不超过物理核心数的进程数。实验报告里可以对比不同 N 下的加速比,说明“并行粒度”的影响。
6. 进阶技巧:用 MPI_Wtime 和非阻塞通信把实验报告写出深度
如果你想把实验报告从“能跑”提升到“有分析”,有两个技巧很实用。第一,用MPI_Wtime做细粒度计时。不要只测总时间,而是在每个通信和计算块前后打时间戳,最后在进程 0 用MPI_Reduce收集各进程的耗时,算出最大值、最小值和平均值。这样能看出负载是否均衡。第二,用非阻塞通信MPI_Isend/MPI_Irecv重叠计算和通信。比如在矩阵乘法里,可以在计算当前块的同时预取下一块数据。下面是一个非阻塞乒乓的片段:
MPI_Request reqs[2]; MPI_Status stats[2]; MPI_Isend(&ping, 1, MPI_INT, 1, 0, MPI_COMM_WORLD, &reqs[0]); MPI_Irecv(&pong, 1, MPI_INT, 1, 0, MPI_COMM_WORLD, &reqs[1]); // 这里可以插入其他计算 MPI_Waitall(2, reqs, stats);参数说明:MPI_Isend和MPI_Irecv立即返回,MPI_Waitall等待所有请求完成。注意MPI_Request数组要足够大,MPI_Status可以传MPI_STATUSES_IGNORE。非阻塞通信能避免死锁,但增加了代码复杂度,实验报告里可以对比阻塞和非阻塞版本的耗时。
我自己的习惯是:每次做 MPI 实验,先写一个最小可运行的点对点程序,确认环境没问题,再往上叠集合通信和矩阵计算。遇到卡死,第一反应是检查发送和接收的匹配关系,第二反应是看进程数是否超过了硬件支持。这份实验报告的价值不在于代码多长,而在于你亲手踩过那些坑之后,对“并行”这件事有了肌肉记忆。希望帮到你。
本文还有配套的精品资源,点击获取