并行程序设计——OMP编程
实验一
实验内容
分别实现课件中的梯形积分法的Pthread、OpenMP版本,熟悉并掌握OpenMP编程方法,探讨两种编程方式的异同。
实验代码
OpenMP编程
#include <stdio.h>
#include <stdlib.h>
#include <omp.h>
void Trap(double a, double b, int n, double *global_result_p);
double f(double x) {
return x * x;
}
int main(int argc, char *argv[]) {
double global_result = 0.0;
double a, b;
int n;
int thread_count;
thread_count = 5;
printf("Enter a, b, and n\n");
scanf("%lf %lf %d", &a, &b, &n);
# pragma omp parallel num_threads(thread_count)
Trap(a, b, n, &global_result);
printf("With n = %d trapezoids, our estimate\n", n);
printf("of the integral from %f to %f = %.14e\n", a, b, global_result);
return 0;
} /* main*/
void Trap(double a, double b, int n, double *global_result_p) {
double h, x, my_result;
double local_a, local_b;
int i, local_n;
int my_rank = omp_get_thread_num();
int thread_count = omp_get_num_threads();
h = (b - a) / n;
local_n = n / thread_count;
local_a = a + my_rank * local_n * h;
local_b = local_a + local_n * h;
my_result = (f(local_a) + f(local_b)) / 2.0;
for (i = 1; i <= local_n; i++) {
x = local_a + i * h;
my_result += f(x);
}
my_result = my_result * h;
# pragma omp critical
*global_result_p += my_result;
} /* Trap*/
实验效果
pthread编程
#include <pthread.h>
#include <algorithm>
#include <stdio.h>
# include <stdlib.h>
double f(double x) {
return x * x;
}
typedef struct {
int threadId;
} threadParm_t;
const int THREAD_NUM = 5; //表示线程的个数
int next_task = 1;//从1开始
int seg = 50;//每次分50份
double totalSize = 0.000; //表示总面积
double h = 0.00;//表示间距
double a = 1.000, b = 8.000;
int n = 1000;
pthread_mutex_t barrier_mutex = PTHREAD_MUTEX_INITIALIZER;
void cal(int i) {
//计算第i分
double x = a + i * h;
double myResult = f(x) * h;
pthread_mutex_lock(&barrier_mutex);
totalSize += myResult;
pthread_mutex_unlock(&barrier_mutex);
}
void *SSE_pthread(void *parm) {
int task = 0;
while (true) {
pthread_mutex_lock(&barrier_mutex);
next_task += seg;
task = next_task;
pthread_mutex_unlock(&barrier_mutex);
if (task - seg >= n) break;
for (int i = task - seg; i < std::min(task, n); i++) {
cal(i);
}
}
pthread_exit(nullptr);
}
int main(int arg, char *argv[]) {
pthread_t thread[THREAD_NUM];
threadParm_t threadParm[THREAD_NUM];
for (int i = 0; i < THREAD_NUM; i++) {
threadParm[i].threadId = i;
}
printf("Enter a, b, and n\n");
scanf("%lf %lf %d", &a, &b, &n);
//先计算两边的面积之和
h = (b - a) / n;
totalSize = h * (f(a) + f(b)) / 2;
for (int i = 0; i < THREAD_NUM; i++) {
pthread_create(&thread[i], nullptr, SSE_pthread, (void *) &threadParm[i]);
}
for (int k = 0; k < THREAD_NUM; k++) {
pthread_join(thread[k], nullptr);
}
pthread_mutex_destroy(&barrier_mutex);
printf("With n = %d trapezoids, our estimate\n", n);
printf("of the integral from %f to %f = %.14e\n", a, b, totalSize);
return 0;
}
实验结果
注:由于我的pthread
求梯形的基准和OpenMP的基准不同,所以求的实验结果略有差异
两种编程方式的异同
OpenMP
是根植于编译器的,更偏向于将原来串行化的程序,通过加入一些适当的编译器指令(compiler directive
)变成并行执行,从而提高代码运行的速率。这样做是在没有OpenMP
支持编译时,代码仍然可以编译。 创建线程等后续工作需要编译器来完成。
而Pthread
是一个库,所有的并行线程创建都需要我们自己完成。Pthread
仅在有多个处理器可用时才对并行化有效,并且仅在代码针对可用处理器数进行了优化时才有效。 因此,OpenMP
的代码更易于扩展。
实验二
实验描述
对于课件中“多个数组排序”的任务不均衡案例进行OpenMP编程实现(规模可自己调整),并探索不同循环调度方案的优劣。提示:可从任务分块的大小、线程数的多少、静态动态多线程结合等方面进行尝试,探索规律。
实验代码
#include <stdio.h>
#include <stdlib.h>
#include <omp.h>
#include <ctime>
#include <sys/time.h>
#include <algorithm>
const int ARR_NUM = 1000; //固定数组大小
const int ARR_LEN = 4000; //固定数组长度
int THREAD_NUM = 4; //固定线程个数,改变粗颗粒分配大小
int seg = 10; //改变粗颗粒的分配大小
struct timeval startTime, stopTime;// timers
int arr[ARR_NUM][ARR_LEN];
int tempArr[ARR_NUM][ARR_LEN]; //用于暂时存储数组,控制变量唯一
int next_arr = 0;
void OMP_sort();
void init(int num) {
srand(unsigned(time(nullptr)));
for (int i = 0; i < num; i++) {
for (int j = 0; j < ARR_LEN; j++)
arr[i][j] = rand();
}
}
//使用冒泡排序,使变量唯一
void sort(int *a) {
for (int i = 0; i < ARR_LEN - 1; ++i) {
for (int j = i + 1; j < ARR_LEN; ++j) {
if (a[j] < a[i]) {
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
}
}
void store(int a[][ARR_LEN], int b[][ARR_LEN], int num) {
for (int i = 0; i < num; i++) {
for (int j = 0; j < ARR_LEN; j++)
b[i][j] = a[i][j];
}
}
int main(int argc, char *argv[]) {
init(ARR_NUM);
store(arr, tempArr, ARR_NUM);
for (; seg <= 100; seg += 10) {
printf("seg: %d\n",seg);
store(tempArr, arr, ARR_NUM);
next_arr = 0;
gettimeofday(&startTime, NULL);
# pragma omp parallel num_threads(THREAD_NUM)
OMP_sort();
gettimeofday(&stopTime, NULL);
double trans_mul_time =
(stopTime.tv_sec - startTime.tv_sec) * 1000 + (stopTime.tv_usec - startTime.tv_usec) * 0.001;
printf("time :%lf ms\n", trans_mul_time);
}
return 0;
} /* main*/
void OMP_sort() {
int task = 0;
while (true) {
#pragma omp critical //每次只能一个线程执行
task = next_arr += seg;//每次动态分配seg个
if (task - seg >= ARR_NUM) break;
int min = task < ARR_NUM ? task : ARR_NUM;
for (int i = task - seg; i < min; ++i) {
sort(arr[i]);
}
}
} /* Trap*/
实验运行结果
如此进行多次重复实验,将得到的数据绘成echarts
图表
实验图表和结论
实验结论
- 由以上图表可知,根据动态粗颗粒的分配的大小可知,各个分配结果的运行效率基本相似。
- 由上图可知,当每次分配数目为30的时候,运行时间最短,运行效率最快。但是当每次分配个数超过80时,运行效率也不低。由于此次实验总的数组的行数比较少,所以运行效率并不会相差很大,如若需要更加准确的数值的话,则需要加大数组的行数。
实验三(附加题)
实验内容
实现高斯消去法解线性方程组的OpenMP编程,与SSE/AVX编程结合,并探索优化任务分配方法。
实验代码
#include <stdio.h>
#include <stdlib.h>
#include <omp.h>
#include <ctime>
#include <sys/time.h>
#include <algorithm>
#include <pmmintrin.h>
const int n = 2048;//固定矩阵规模,控制变量
const int maxN = n + 1; // 矩阵的最大值
float a[maxN][maxN];
float temp[maxN][maxN];//用于暂时存储a数组中的变量,控制变量唯一
const int THREAD_NUM = 4; //表示线程的个数
int seg = 30;//表示每次线程运行的行数,将来它会改变来探究最优任务分配方法
int next_task = 0;
int line = 0;//记录当前所依赖的行数
struct timeval startTime, stopTime;// timers
/**
* 根据第i行的元素,消除j行的元素
* @param i 根据的行数
* @param j 要消元的行数
*/
void OMP_elimination(int i, int j) {
float tep;
__m128 div, t1, t2, sub;
// 用temp暂存相差的倍数
tep = a[j][i] / a[i][i];
// div全部用于存储temp,方便后面计算
div = _mm_set1_ps(tep);
//每四个一组进行计算,思想和串行类似
int k = n - 3;
for (; k >= i + 1; k -= 4) {
t1 = _mm_loadu_ps(a[i] + k);
t2 = _mm_loadu_ps(a[j] + k);
sub = _mm_sub_ps(t2, _mm_mul_ps(t1, div));
_mm_store_ss(a[j] + k, sub);
}
//处理剩余部分
for (k += 3; k >= i + 1; --k) {
a[j][k] -= a[i][k] * tep;
}
a[j][i] = 0.00;
}
/**
* 多线程消元函数,动态粗颗粒分配,每次分配seg个
* @param parm
*/
void OMP_func() {
int task = 0;
while (true) {
#pragma omp critical //每次只能一个线程执行
{
task = next_task;
next_task += seg;//每次分配seg个
}
if (task >= n) break;
int min = task + seg < n ? task + seg : n;
for (int i = task; i < min; ++i) {
OMP_elimination(line, i);
}
}
}
//用于矩阵改变数值,为防止数据溢出,随机数的区间为100以内的浮点数
void change() {
srand((unsigned) time(NULL));
for (int i = 0; i < n; i++) {
for (int j = 0; j <= n; j++) {
a[i][j] = (float) (rand() % 10000) / 100.00;
}
}
}
/**
* 将a数组的数据存储到b数组中
* @param a
* @param b
*/
void store(float a[][maxN], float b[][maxN]) {
for (int i = 0; i < n; i++) {
for (int j = 0; j <= n; j++) {
b[i][j] = a[i][j];
}
}
}
int main(int arg, char *argv[]) {
change();
store(a, temp);
// SSE算法消元设计
for (; seg <= 300; seg += 30) {
store(temp, a);//从temp中取数
printf("seg: %d\n", seg);
gettimeofday(&startTime, NULL);
for (line = 0; line < n - 1; ++line) {
next_task = line + 1;
# pragma omp parallel num_threads(THREAD_NUM)
OMP_func();
}
gettimeofday(&stopTime, NULL);
double trans_mul_time =
(stopTime.tv_sec - startTime.tv_sec) * 1000 + (stopTime.tv_usec - startTime.tv_usec) * 0.001;
printf("time: %lf ms\n", trans_mul_time);
}
}
代码运行结果
如此重复多次实验,将所得到的的数据绘成echarts
图表
实验图表和结论
实验结论
- 与上个实验不同的是,这次实验我增加了整体矩阵的大小和每次分配行数的间距,这样得出的实验结果更加准确。
- 由上图表可知,当矩阵规模达到2048 × \times × 2048时,整体运行的运行时间都相对较短,运行效率相比于串行化执行要快很多。每次分配90行的时候运行效果最佳,180~240的时候与90相差不大,整体区别不太。所以矩阵规模越大,尽量分配越多的行数使运行效率加快。