一、实验题目:
用 MPI 技术设计实现高斯算法。
二、实验目的
熟悉 MPI 编程,加深对其编程的理解,并能够使用MPI进行程序设计。
三、实验环境
Linux Centos, 运行 MPI
四、程序设计
整体逻辑
检查命令行参数并获取n值。
初始化MPI环境。
获取当前进程的rank和进程总数。
记录开始时间。
计算每个进程的工作范围(local_start和local_end)。
计算当前进程的局部和(local_sum)。
使用MPI_Reduce函数将所有进程的local_sum归约到总和(total_sum)中。
记录结束时间。
如果当前进程是主进程(rank为0),打印计算结果和执行时间。
结束MPI环境。
关键函数及其相关用法
(1)MPI_Init()
用法:int MPI_Init(int *argc, char ***argv);
作用:初始化MPI环境。在使用MPI的程序中,此函数需要在所有其他MPI函数之前调用。
(2)MPI_Comm_rank()
用法:int MPI_Comm_rank(MPI_Comm comm, int *rank);
作用:获取当前进程在通信器(comm)中的编号(rank)。在程序中,我们将此编号作为进程的rank,并据此确定进程的工作范围。
(3)MPI_Comm_size()
用法:int MPI_Comm_size(MPI_Comm comm, int *size);
作用:获取通信器(comm)中的进程总数。在程序中,我们使用进程总数来计算每个进程的工作范围。
(4)MPI_Wtime()
用法:double MPI_Wtime(void);
作用:返回当前时间(以秒为单位),可用于测量代码的执行时间。在程序中,我们通过计算结束时间与开始时间的差值来计算总执行时间。
(5)MPI_Reduce()
用法:int MPI_Reduce(const void *sendbuf, void *recvbuf, int count, MPI_Datatype datatype, MPI_Op op, int root, MPI_Comm comm);
作用:将所有进程的sendbuf中的数据进行归约操作(如求和、最大值等),并将结果存储在根进程(root)的recvbuf中。在程序中,我们使用MPI_SUM操作将所有进程的局部和(local_sum)归约到总和(total_sum)中。
(6)MPI_Finalize()
用法:int MPI_Finalize(void);
作用:结束MPI环境。在使用MPI的程序中,此函数需要在所有其他MPI函数之后调用。
代码实现
#include <stdio.h>
#include <stdlib.h>
#include <mpi.h>
int main(int argc, char *argv[]) {
int rank, size;
long long n, local_start, local_end, local_sum, total_sum;
double start_time, end_time;
// 检查命令行参数是否正确
if (argc != 2) {
printf("Usage: %s <n>\n", argv[0]);
return 1;
}
// 从命令行参数获取n值
n = atoll(argv[1]);
// 初始化MPI环境
MPI_Init(&argc, &argv);
// 获取当前进程的rank(编号)
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
// 获取进程总数
MPI_Comm_size(MPI_COMM_WORLD, &size);
// 记录开始时间
start_time = MPI_Wtime();
// 计算每个进程的工作范围(local_start和local_end)
long long chunk_size = n / size;
local_start = rank * chunk_size + 1;
local_end = (rank == size - 1) ? n : (rank + 1) * chunk_size;
// 计算当前进程的局部和(local_sum)
local_sum = (local_start + local_end) * (local_end - local_start + 1) / 2;
// 使用MPI_Reduce函数将所有进程的local_sum归约到总和(total_sum)中
MPI_Reduce(&local_sum, &total_sum, 1, MPI_LONG_LONG_INT, MPI_SUM, 0, MPI_COMM_WORLD);
// 记录结束时间
end_time = MPI_Wtime();
// 如果当前进程是主进程(rank为0),打印计算结果和执行时间
if (rank == 0) {
printf("The sum of integers from 1 to %lld is %lld.\n", n, total_sum);
printf("Execution time: %lf seconds.\n", end_time - start_time);
}
// 结束MPI环境
MPI_Finalize();
return 0;
}
五、运行结果
编译:mpicc -o gauss_MPI gauss_MPI.c
运行:mpirun -np 4 ./gauss_MPI <n>
<n>替换为要计算整数和的上限
六、实验心得
程序编写技巧
- 包含头文件:代码中包含了一些标准头文件和MPI头文件,以便使用相关的函数和类型。例如,<stdio.h>、<stdlib.h>是标准头文件,<mpi.h>是MPI头文件。
- 命令行参数处理:代码使用argc和argv参数来接收命令行输入的参数。在这里,程序期望接收一个整数作为命令行参数,表示计算的范围。如果参数数量不正确,程序会打印用法信息并返回。
- MPI初始化和结束:通过调用MPI_Init和MPI_Finalize函数来初始化和结束MPI环境。这确保了MPI库的正确使用,并且每个进程都在正确的时间点开始和结束MPI环境。
- 进程等级和总进程数获取:使用MPI_Comm_rank和MPI_Comm_size函数分别获取通信域中的进程数量和当前进程的等级。这些信息对于进程间的任务分配和协调非常重要。
- 计算任务分配:通过计算每个进程的工作范围来划分计算任务。每个进程计算一部分整数的和,通过local_start和local_end来确定本地计算范围。
- 局部和计算:每个进程计算自己的局部和,通过local_start和local_end计算局部整数的和。
- 归约操作:使用MPI_Reduce函数将所有进程的局部和归约到总和中。这里使用了MPI_SUM操作,将所有进程的局部和相加,最终将结果放入主进程(rank为0)的total_sum变量中。
- 打印结果和执行时间:在主进程中,打印计算结果和执行时间信息。
- 记录执行时间:使用MPI_Wtime函数记录程序的开始时间和结束时间,计算执行时间。
函数使用技巧
- 命令行参数处理:通过接受命令行参数来灵活地获取用户输入的参数值。代码中使用argc和argv参数来获取命令行参数的数量和值,并进行合法性检查。这种技巧使得程序可以通过命令行参数进行配置和定制。
- MPI初始化和结束:使用MPI_Init和MPI_Finalize函数在程序的开始和结束分别初始化和结束MPI环境。这种技巧确保了MPI库的正确使用,保证了每个进程在正确的时间点开始和结束MPI环境。
- 进程等级和总进程数获取:使用MPI_Comm_rank和MPI_Comm_size函数获取通信域中的进程数量和当前进程的等级。这些信息对于进程间的任务分配和协调非常重要。进程等级用于区分不同的进程,而进程数量用于确定任务划分的方式。
- 归约操作:通过使用MPI_Reduce函数将每个进程的局部求和归约到总和中。这种技巧利用了MPI提供的归约操作,通过对进程间的通信和协调来获得全局的结果。
- 执行时间测量:使用MPI_Wtime函数测量程序的执行时间。这个函数返回一个双精度浮点数,表示从MPI环境初始化到调用该函数的时间间隔。这种技巧允许程序在分布式环境中测量并报告程序的执行时间。
容易出错的地方
- 命令行参数处理:代码假设命令行参数的格式是正确的,即第一个参数是整数n。如果用户提供了不符合要求的参数格式,例如非整数参数或没有提供足够的参数,代码可能会出现错误。
- 工作范围划分:在将n划分为均匀的工作块时,代码假设n可以被进程数量整除,以获得均匀的工作划分。如果n不能被进程数量整除,可能会导致工作量不均衡,最后一个进程可能需要处理额外的工作。
- 归约操作的数据类型匹配:使用MPI_Reduce函数进行归约操作时,要确保传递的数据类型与归约操作的数据类型匹配。在这段代码中,使用MPI_LONG_LONG_INT作为数据类型,确保与local_sum和total_sum的类型匹配。
- 错误处理:MPI函数返回的错误代码可以用于检测并处理MPI调用中的错误。在实际的MPI程序中,通常需要对MPI函数的返回值进行检查,并根据错误码采取相应的错误处理措施。在这段代码中,没有对MPI函数的返回值进行错误处理。
- 整数溢出:在这段代码中,使用了long long类型来存储大整数。但是,如果输入的n值超过了long long类型的表示范围,可能会导致整数溢出和计算错误。
心得体会
使用MPI时,确保已正确包含头文件#include <mpi.h>,并在编译时使用相应的MPI编译器(如mpicc)。
使用MPI_Wtime()测量执行时间。此函数提供了较高的时间精度,比使用clock()函数更为准确。
当处理命令行参数时,使用atoll()函数可以将字符串转换为长整型。在处理大整数时,这比使用atoi()函数更合适。
选择合适的chunk_size以平衡负载。在这个示例中,我们假设所有进程的计算速度相同,因此将任务平均分配给各个进程。然而,在实际应用中,可能需要根据具体情况动态调整chunk_size。
在编写MPI程序时,务必注意正确使用通信器(如MPI_COMM_WORLD),并确保在使用通信函数(如MPI_Reduce())时指定正确的根进程(root)和操作(如MPI_SUM)。
在并行计算中,确保各进程的局部和正确计算和归约。注意检查边界条件,如最后一个进程的工作范围。