一、实验题目:
用 OpenMP 技术设计实现高斯算法。
二、实验目的
熟悉 OpenMP 编程,加深对其编程的理解,并能够使用OpenMP进行程序设计。
三、实验环境
Linux, 运行 OpenMP
四、程序设计
整体逻辑
检查命令行参数并获取n值。
记录开始时间。
创建并行区域。
计算每个线程的工作范围(local_start和local_end)。
计算当前线程的局部和(local_sum)。
使用critical指令保护对total_sum的更新,确保互斥访问。
结束并行区域。
记录结束时间。
打印计算结果和执行时间。
关键函数及其相关用法
(1)omp_get_wtime()
用法:double omp_get_wtime(void);
作用:返回当前时间(以秒为单位),可用于测量代码的执行时间。在程序中,我们通过计算结束时间与开始时间的差值来计算总执行时间。
(2)omp_get_thread_num()
用法:int omp_get_thread_num(void);
作用:返回调用线程在其所属并行区域中的唯一ID(从0开始)。在程序中,我们将此ID作为线程的rank,并据此确定线程的工作范围。
(3)omp_get_num_threads()
用法:int omp_get_num_threads(void);
作用:返回当前并行区域中的线程总数。在程序中,我们使用线程总数来计算每个线程的工作范围。
代码实现
#include <stdio.h>
#include <stdlib.h>
#include <omp.h>
int main(int argc, char* argv[]) {
// 检查命令行参数是否正确
if (argc != 2) {
printf("Usage: %s <n>\n", argv[0]);
return 1;
}
// 从命令行参数获取n值
long long n = atoll(argv[1]);
long long total_sum = 0;
double start_time, end_time;
// 记录开始时间
start_time = omp_get_wtime();
// 创建并行区域
#pragma omp parallel
{
long long local_start, local_end, local_sum;
// 获取当前线程的rank(编号)
int rank = omp_get_thread_num();
// 获取线程总数
int size = omp_get_num_threads();
// 计算每个线程的工作范围(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;
// 使用critical指令保护对total_sum的更新,确保互斥访问
#pragma omp critical
total_sum += local_sum;
}
// 记录结束时间
end_time = omp_get_wtime();
// 打印计算结果和执行时间
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);
return 0;
}
五、运行结果
编译:gcc -fopenmp -o gauss_OpenMP gauss_OpenMP.c
运行:./gauss_OpenMP <n>
<n>替换为要计算整数和的上限
六、实验心得
程序编写技巧
- 包含头文件:代码中包含了一些标准头文件和OpenMP头文件,以便使用相关的函数和类型。例如,
<stdio.h>
、<stdlib.h>
是标准头文件,<omp.h>
是OpenMP头文件。 - 命令行参数处理:通过命令行参数获取整数n的值,并检查参数的正确性。代码假设命令行参数的格式是正确的,即第一个参数是整数n。如果用户提供了不符合要求的参数格式,例如非整数参数或没有提供足够的参数,代码可能会出现错误。
- 并行区域:使用
#pragma omp parallel
创建并行区域。这个指令将下面的代码块标记为并行执行。在这段代码中,所有并行线程都会执行标记的代码块。 - 线程编号和线程总数获取:使用
omp_get_thread_num
和omp_get_num_threads
函数分别获取当前线程的编号(rank)和线程的总数。这些信息对于线程间的任务分配和协调非常重要。 - 工作范围划分:根据线程编号,计算每个线程的工作范围(local_start和local_end)。使用整除运算符将n划分为均匀的工作块,确保每个线程负责一部分工作。
- 局部求和计算:在每个线程中,根据工作范围计算当前线程的局部求和(local_sum)。这里使用了求和公式来计算给定范围内整数的和。
- 互斥访问:使用
#pragma omp critical
指令来保护对共享变量total_sum
的更新。这确保了每个线程在更新total_sum
时的互斥访问,避免了竞争条件。 - 执行时间测量:使用
omp_get_wtime
函数测量程序的执行时间。这个函数返回一个双精度浮点数,表示从计时器启动到调用该函数的时间间隔。这种技巧允许程序在并行环境中测量并报告程序的执行时间。 - 打印结果和执行时间:在主线程中,打印计算结果和程序的执行时间。这里使用了
printf
函数来格式化和输出结果。
函数使用技巧
- 命令行参数处理:代码通过命令行参数获取用户输入的参数值。通过使用
argc
和argv
参数,程序可以接受用户在命令行中提供的输入,并对其进行处理。这种技巧使得程序可以灵活地接受用户定义的输入。 - 并行区域创建:通过使用
#pragma omp parallel
指令,创建了一个并行区域。该指令将下面的代码块标记为并行执行。在这个并行区域中,代码将被多个线程同时执行,从而实现并行计算。 - 线程编号和线程总数获取:通过调用
omp_get_thread_num
和omp_get_num_threads
函数,获取当前线程的编号和线程的总数。这些信息对于线程间的任务分配和协调非常重要。线程编号可以用于区分不同的线程,线程总数则用于确定任务划分的方式。 - 互斥访问保护:通过使用
#pragma omp critical
指令,对对共享变量total_sum
的更新进行互斥访问保护。这样可以确保在任意时刻只有一个线程能够访问或修改共享变量,从而避免了竞争条件和数据不一致的问题。 - 执行时间测量:通过使用
omp_get_wtime
函数,测量程序的执行时间。这个函数返回一个双精度浮点数,表示从计时器启动到调用该函数的时间间隔。这种技巧允许程序在并行环境中测量并报告程序的执行时间。 - 并行计算和互斥访问的结合:代码中通过并行计算和互斥访问的结合,实现了多线程下的并行求和。每个线程负责一部分工作,通过互斥访问保证了共享变量的正确更新。这种技巧可以提高并行计算的效率和可靠性。
容易出错的地方
- 命令行参数处理:代码假设命令行参数的格式是正确的,即第一个参数是整数n。如果用户提供了不符合要求的参数格式,例如非整数参数或没有提供足够的参数,代码可能会出现错误。
- 并行区域创建和互斥访问:在使用OpenMP创建并行区域时,需要确保共享变量的互斥访问得到正确的保护。在这段代码中,使用了
#pragma omp critical
来保护对共享变量total_sum
的更新。但是,如果其他地方也使用了#pragma omp critical
来访问相同的共享变量,可能会导致死锁或性能下降。 - 线程总数的控制:代码假设并行区域中的线程总数是系统默认的线程总数。然而,实际情况可能与预期不同,因为线程总数可能受到OpenMP环境变量或编译器指令的影响。在编写代码时,要确保线程总数与预期一致,或者显式地控制线程总数。
- 局部变量的正确性:在并行区域内定义的局部变量,如
local_start
、local_end
和local_sum
,需要确保在每个线程中具有正确的值。这些变量的计算和使用应该与线程编号和线程总数相关联,以确保正确的工作范围划分和局部求和计算。 - 互斥访问的性能开销:互斥访问会导致线程之间的竞争和等待,从而引入一定的性能开销。在处理共享变量时,要考虑到互斥访问可能对程序性能的影响,并在必要时寻找更适合的并行算法或数据共享策略。
- 线程间数据依赖:在并行计算中,如果存在线程间的数据依赖关系,可能会导致结果错误或性能下降。在编写并行代码时,需要注意线程间的数据依赖关系,并确保正确的同步或数据共享机制。
心得体会
使用OpenMP时,确保要正确包含头文件#include <omp.h>,并在编译时添加-fopenmp选项。
使用omp_get_wtime()测量执行时间。此函数提供了较高的时间精度,比使用clock()函数更为准确。
当使用critical指令时,务必仔细确认需要保护的代码范围。过大的临界区可能导致性能下降,过小的临界区可能导致数据竞争和不正确的结果。
选择合适的chunk_size以平衡负载。在这个示例中,我们假设所有线程的计算速度相同,因此将任务平均分配给各个线程。然而,在实际应用中,可能需要根据具体情况动态调整chunk_size。
当处理命令行参数时,使用atoll()函数可以将字符串转换为长整型。在处理大整数时,这比使用atoi()函数更合适。