简介:这份郑州大学计算机与人工智能学院并行计算实验报告,由gyb老师指导完成,作者在该课程中取得90分以上成绩,适合正在修读并行计算、高性能计算相关课程的高校学生,以及希望系统理解并行编程实践的学习者参考。报告围绕任务分解、负载均衡、通信与同步等基础理论展开,涵盖共享内存与分布式内存两类并行计算模型,并涉及矩阵乘法、快速傅里叶变换、归并排序与快速排序等算法的并行化设计思路。图像绘制部分介绍了并行渲染、图像分割及像素操作加速策略,代码分析部分则结合性能瓶颈识别、并行度评估与缓存效率分析,提及Gprof、Intel VTune等性能剖析工具的使用。实验环境覆盖OpenMP、MPI以及CUDA、OpenCL等GPU编程平台,末尾附有实验总结与深度学习、高性能计算方向的展望。资源以zip压缩包形式提供,整体约37.69MB,内含实验报告文档、绘制图像与代码分析材料,已有699人学习,便于对照复现实验、理解并行计算核心概念并提升实际编程与问题解决能力。
1. 并行计算实验报告怎么写才能拿 90+:从绘图到代码分析的完整拆解
很多人做并行计算实验,代码跑通了、加速比也测出来了,最后报告只拿个及格分。问题往往不在程序本身,而在报告没有把「并行」这件事讲清楚——老师想看到的是你对加速比曲线的解读、对通信开销的分析、对串行与并行结果一致性的验证,而不是一堆截图堆砌。这个标题指向的是一份课程实验报告,核心诉求是:实验代码能跑、图表能说明问题、分析能体现思考深度。适合正在修并行计算、高性能计算课程的同学,也适合想系统梳理 MPI/OpenMP 实操经验但不知道从哪下手的开发者。接下来我按「先跑通、再画图、后分析」的顺序,把每个环节的坑和技巧拆开讲。
2. 实验环境搭建与第一个 MPI 程序:别让环境问题吃掉你两小时
2.1 选 MPI 还是 OpenMP:先看你的实验要测什么
并行计算实验通常分两类:共享内存用 OpenMP,分布式内存用 MPI。选错了不是不能跑,而是测出来的加速比没有意义。判断标准很简单——如果你的实验是矩阵乘法、向量加法这类数据可以整块放在一台机器内存里的,用 OpenMP 就够了,编译时加-fopenmp,代码改动量极小。如果实验要求多节点通信、或者题目明确写了「消息传递」,那就必须上 MPI。
我一般建议两个都做一遍对比,因为报告里如果能同时展示 OpenMP 的线程级并行和 MPI 的进程级并行,分析维度会丰富很多。但注意:不要在一个实验里混用两种模型去测同一个指标,否则加速比的基准就乱了。
环境方面,Linux 下装 OpenMPI 最省事:
# Ubuntu/Debian 系 sudo apt update sudo apt install openmpi-bin libopenmpi-dev # 验证安装 mpirun --version装完之后用mpirun -np 4 ./your_program测试能否正常启动 4 个进程。如果报错mpirun: command not found,检查 PATH;如果报ORTE_ERROR之类,大概率是主机名解析问题,在/etc/hosts里把本机 hostname 映射到 127.0.0.1 即可。
2.2 最小可运行 MPI 程序:点对点通信的骨架
下面这个程序做一件事:进程 0 发一个数组给进程 1,进程 1 收到后打印。别看简单,实验报告里所有复杂的通信模式都是从这延伸出去的。
#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); // 总进程数 if (size < 2) { if (rank == 0) printf("至少需要 2 个进程\n"); MPI_Finalize(); return 0; } int data[5] = {1, 2, 3, 4, 5}; MPI_Status status; if (rank == 0) { MPI_Send(data, 5, MPI_INT, 1, 0, MPI_COMM_WORLD); printf("进程 0 已发送数据\n"); } else if (rank == 1) { int recv[5]; MPI_Recv(recv, 5, MPI_INT, 0, 0, MPI_COMM_WORLD, &status); printf("进程 1 收到: "); for (int i = 0; i < 5; i++) printf("%d ", recv[i]); printf("\n"); } MPI_Finalize(); // 清理 MPI 环境 return 0; }编译命令:mpicc -o p2p p2p.c,运行:mpirun -np 2 ./p2p。
参数说明:MPI_Send的第二个参数是发送元素个数,第三个是数据类型,第四个是目标进程 rank,第五个是消息标签(tag),第六个是通信域。MPI_Recv多一个MPI_Status指针,用来获取实际收到的数据量和来源。标签的作用是区分同一对进程之间的不同消息流,实验里如果有多轮通信,标签写错会导致收错数据——这是新手最常见的翻车点之一。
2.3 编译与运行时的三个高频报错
第一个:mpirun启动后卡住不动。原因通常是MPI_Send和MPI_Recv不匹配,比如进程 0 发给了进程 1,但进程 1 在等进程 2 的数据。解决办法是画一张通信时序图,确认每个 Send 都有对应的 Recv。
第二个:段错误(Segmentation fault)。九成是缓冲区大小不对,比如发送 5 个 int 但接收方只开了 3 个 int 的空间。MPI 不会帮你检查这个,它只负责搬字节。
第三个:结果对但加速比是负数或大于进程数。这通常是因为计时方式不对——用了clock()而不是MPI_Wtime()。clock()测的是 CPU 时间,多线程下会累加,必须换成MPI_Wtime()或omp_get_wtime()。
3. 加速比与效率曲线怎么画:让图表替你说话
3.1 测什么、怎么测:计时点的选择决定数据可信度
实验报告里最核心的图就是加速比曲线和效率曲线。加速比 = 串行时间 / 并行时间,效率 = 加速比 / 进程数。听起来简单,但计时点选错,整条曲线就是废的。
我一般这样做:串行版本单独编译一个不带 MPI 的可执行文件,用MPI_Wtime()或clock_gettime()测纯计算部分。并行版本在MPI_Init之后、MPI_Finalize之前计时,但要排除掉初始化通信域和第一次通信的冷启动开销。具体做法是:先跑一次预热,再跑三次取平均。
double start, end; MPI_Barrier(MPI_COMM_WORLD); // 确保所有进程同步到同一起跑线 start = MPI_Wtime(); // 你的核心计算或通信代码 MPI_Barrier(MPI_COMM_WORLD); // 确保所有进程都完成 end = MPI_Wtime(); if (rank == 0) { printf("耗时: %f 秒\n", end - start); }注意MPI_Barrier不能省。不加的话,进程 0 可能已经跑完了,进程 3 还在初始化,测出来的时间是错的。
3.2 用 Python 画加速比曲线:模板直接抄
测完数据后,用 matplotlib 画图。下面这个脚本读一个 CSV 文件,画加速比和效率两条曲线。
import matplotlib.pyplot as plt import pandas as pd # CSV 格式: procs,time df = pd.read_csv("timing.csv") serial_time = df["time"].iloc[0] # 第一行是串行时间 df["speedup"] = serial_time / df["time"] df["efficiency"] = df["speedup"] / df["procs"] fig, ax1 = plt.subplots(figsize=(8, 5)) ax1.plot(df["procs"], df["speedup"], "o-", color="tab:blue", label="加速比") ax1.plot(df["procs"], df["procs"], "--", color="gray", label="理想线性") ax1.set_xlabel("进程数") ax1.set_ylabel("加速比") ax1.set_title("加速比与效率曲线") ax2 = ax1.twinx() ax2.plot(df["procs"], df["efficiency"], "s-", color="tab:red", label="效率") ax2.set_ylabel("效率") ax2.set_ylim(0, 1.1) lines1, labels1 = ax1.get_legend_handles_labels() lines2, labels2 = ax2.get_legend_handles_labels() ax1.legend(lines1 + lines2, labels1 + labels2, loc="center right") plt.tight_layout() plt.savefig("speedup.png", dpi=150) plt.show()逻辑说明:左轴画加速比,右轴画效率,灰色虚线是理想线性加速比。这样一张图能同时说明两个问题——你的程序离理想情况差多少,以及增加进程后效率衰减有多快。参数上,dpi=150保证截图放进报告不糊,tight_layout()防止标签被裁掉。
3.3 曲线不好看怎么办:三种常见形态的解读
第一种:加速比远低于线性。原因通常是通信开销占比太大,或者负载不均衡。报告里要算一下通信时间占总时间的比例,如果超过 30%,说明问题规模太小,进程间同步的成本盖过了计算收益。
第二种:加速比超过进程数(超线性加速)。这通常是因为缓存效应——并行后每个进程处理的数据块变小,能全部放进 L2 缓存,而串行版本数据太大一直访问内存。这不是错误,但要在报告里解释清楚,否则老师会以为你数据造假。
第三种:增加进程后时间反而变长。检查是不是进程数超过了 CPU 物理核心数,导致频繁上下文切换。用lscpu看一下核心数,进程数不要超过物理核心数。
4. 代码分析部分怎么写:从「能跑」到「讲清楚为什么这样跑」
4.1 通信模式分析:点对点、广播、归约的适用场景
实验报告里如果只贴代码不说通信模式,分析深度就不够。MPI 的通信原语分三类:点对点(Send/Recv)、集合通信(Bcast/Reduce/Scatter/Gather)、同步(Barrier)。每种都有适用场景。
点对点适合有依赖关系的任务链,比如流水线。广播适合把参数分发给所有进程。归约适合把各进程的部分结果汇总,比如求全局和。下面是一个归约的例子:
double local_sum = 0.0; for (int i = rank; i < N; i += size) { local_sum += data[i]; } double global_sum; MPI_Reduce(&local_sum, &global_sum, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD); if (rank == 0) { printf("全局和: %f\n", global_sum); }MPI_Reduce的第二个参数只在 root 进程(这里是 0)上有意义,其他进程传 NULL 也行。MPI_SUM可以换成MPI_MAX、MPI_MIN等。报告里要写清楚:为什么用 Reduce 而不是让每个进程 Send 给 0 再累加?因为 Reduce 的内部实现是树形归约,复杂度是 O(log n),而手动 Send 是 O(n)。
4.2 负载均衡检查:用时间戳找出最慢的那个进程
并行程序的总耗时取决于最慢的进程。如果各进程计算量不均,快的进程要等慢的,加速比就上不去。检查方法很简单:每个进程在计算前后各打一个时间戳,输出到文件,最后对比。
double t_start = MPI_Wtime(); // 计算部分 double t_end = MPI_Wtime(); printf("rank %d: start=%.6f end=%.6f duration=%.6f\n", rank, t_start, t_end, t_end - t_start);跑完之后把输出按 rank 排序,看 duration 那一列。如果最大值和最小值差超过 20%,就说明负载不均衡。解决办法:如果是矩阵分块,改用循环分配而不是块分配;如果是任务队列,用主从模式动态派发。
4.3 串行与并行结果一致性验证:别让浮点误差背锅
报告里必须有一节写「结果验证」。并行计算由于浮点加法不满足结合律,求和顺序变了,结果可能有微小差异。验证方法是:串行结果和并行结果的相对误差小于 1e-6 就算一致。
double diff = fabs(serial_result - parallel_result); double rel_err = diff / fabs(serial_result); if (rel_err < 1e-6) { printf("结果一致,相对误差: %e\n", rel_err); } else { printf("结果不一致!相对误差: %e\n", rel_err); }如果误差很大,先检查是不是漏算了边界元素,再检查归约操作是否覆盖了所有进程。浮点误差不会大到 1e-3 这个量级,出现大误差一定是逻辑问题。
5. 避坑与常见问题:那些让报告扣分的细节
5.1 图表没有坐标轴标签和单位
现象:加速比曲线画出来了,但横轴只写「进程数」,纵轴只写「时间」,没有单位。原因:画图时只关注数据,忽略了可读性。解决:时间必须标「秒」,加速比标「倍」,效率标「百分比」。图标题要写清楚测试的问题规模,比如「N=4096 矩阵乘法加速比」。
5.2 只贴代码不写分析
现象:报告里代码占了 80% 篇幅,分析只有两三行。原因:把实验报告当代码仓库了。解决:每段核心代码后面至少写三句话——这段代码做了什么、为什么这样实现、有什么替代方案。老师看的是你的思考过程,不是代码本身。
5.3 加速比数据只测一次
现象:每个进程数只跑一次,数据波动大。原因:系统负载、缓存状态都会影响单次测量。解决:每个配置跑三次取平均,报告里写「三次测量取均值,标准差小于 5%」。如果标准差大,说明测试环境不稳定,要检查是否有其他进程在抢 CPU。
5.4 进程数超过物理核心数
现象:进程数加到 16 时,时间反而比 8 进程长。原因:物理核心只有 8 个,16 个进程在抢资源。解决:用lscpu查看核心数,进程数不超过物理核心数。如果必须测更多进程,在报告里注明「超线程开启,逻辑核心数 16」。
5.5 忘记在报告里写实验环境
现象:数据都有了,但老师问「你用什么机器跑的」答不上来。原因:觉得环境不重要。解决:报告开头固定写一段——CPU 型号、核心数、内存大小、操作系统版本、MPI 实现及版本。这些信息决定了你的数据能不能被复现。
6. 让报告脱颖而出的一个进阶技巧:用强弱扩展性分析替代单点加速比
大部分同学的实验报告只测一组问题规模下的加速比,这只能说明「小规模下并行有效」。如果你想让报告从 90 分往 95 分走,加一节强弱扩展性分析。
强扩展性:问题规模固定,增加进程数,看时间怎么变。这测的是「同样的活,更多人干能快多少」。弱扩展性:每个进程的问题规模固定,增加进程数的同时增加总问题规模,看时间是否保持不变。这测的是「活变多了,加人能不能扛住」。
具体做法:强扩展性用固定 N=4096,进程数从 1 测到 8。弱扩展性用 N=1024×进程数,保证每个进程处理 1024 个元素。如果弱扩展性下时间基本不变,说明你的程序扩展性很好;如果时间随进程数线性增长,说明通信开销吃掉了所有收益。
# 弱扩展性数据示例 data = { "procs": [1, 2, 4, 8], "N": [1024, 2048, 4096, 8192], "time": [0.12, 0.13, 0.15, 0.19] } # 理想情况下 time 应该基本持平报告里把强扩展和弱扩展画在一张图上,强扩展用实线,弱扩展用虚线,横轴都是进程数。这张图能直接说明你的程序在什么规模下效率最高、在什么规模下开始退化。我当年做实验时就是靠这一节把报告从 88 拉到了 93,老师的评语是「有工程视角,不只是跑通」。
最后一个习惯:每次跑完实验,先把原始数据存成 CSV,再用脚本画图。不要手动抄数据到 Excel 里,一旦要改参数重跑,手动数据全废。这个习惯帮我省了至少三次通宵重测的时间。希望帮到你。
本文还有配套的精品资源,点击获取