[OpenMP] 并行计算入门
OpenMP并行计算入门
个人理解
OpenMP是一种通过共享内存并行系统的多处理器程序设计的编译处理方案,通过预编译指令告诉编译器哪些代码块需要被并行化,通过拷贝代码块实现并行程序。对于循环的并行化我的理解大概是这样的:
- 首先,将循环分成线程数个分组,每个分组执行若干个指令,一个分组代表一个线程
- 其中有一个为主线程,其他的均不是主线程,每个分组分别执行自己组内的代码
- 当所有组别的代码执行完毕之后,在最后会和,通过主线程将结果带回
- 关闭其他所有线程(只留下主线程)
我觉得openmp编程中最需要注意的就是哪些代码块可以被并行化,如果不能,怎么修改能让他可以被并行化,以及考虑好数据依赖、循环依赖、数据竞争等问题,保证运算结果的准确性。
配置
我使用的是ubuntu18.04,配置起来比较简单,一般需要安装gcc的。
sudo apt-get install gcc
在编译之前我们需要加上-fopenmp,然后运行就可以执行我们带openmp的程序了。
gcc -fopenmp filename.cpp -o filename
然后我们运行就可以了。
指令学习
parallel
#pragma omp parallel
parallel的作用是使得其作用的代码块被并行执行n次,n默认为cpu核心数目,也可以通过set方法预先设置parallel的线程数目。
omp_set_num_threads(8);
int num = 0;
#pragma omp parallel private(num)
{
int num = omp_get_thread_num();
printf("thread num:%d\n",num);
fflush(stdout);
}
/*
我解决了输出问题。。
thread num:0
thread num:4
thread num:3
thread num:2
thread num:1
thread num:5
thread num:6
thread num:7
*/
for
#pragma omp parallel for[clause[claue..]]
#pragma omp parallel for
for(int i=0;i<10;i++){
//cout<<omp_get_thread_num()<<endl;
printf("thread num is:%d\n",omp_get_thread_num());
fflush(stdout);
}
/*
输出结果:
thread num is:0
thread num is:0
thread num is:0
thread num is:1
thread num is:2
thread num is:2
thread num is:1
thread num is:1
thread num is:3
thread num is:3
*/
for循环使用parallel for指令很简单,只是要考虑到并行时候的循环依赖问题,下面会讨论到。
注意
从这里开始我都会用printf进行输出,因为我试了试cout输出,发现输出老是有问题,就是会输出空白行,我认为是在并行过程中cout<<value和<<endl被当成了两个代码代码块,当有的线程输出了换行符之后,有的线程刚好也输出换行符,就会导致我们看到很多换行符。所以解决这个问题最好的办法是用printf输出,就不会出现那样的问题了。
sections
sections与for不同的是将代码块分成不同的功能模块,而for做的是将代码块分成不同的数据模块,其特点是“功能并行”,for的并行特点是“数据并行”。例如,通过sections将代码块分成不同的section,每个section可以是功能不同的函数,这就是与for最大的不同。
#pragma omp parallel sections
{
#pragma omp section
{
printf("thread num:%d\n",omp_get_thread_num());
fflush(stdout);
}
#pragma omp section
{
printf("thread num:%d\n",omp_get_thread_num());
fflush(stdout);
}
#pragma omp section
{
printf("thread num:%d\n",omp_get_thread_num());
fflush(stdout);
}
#pragma omp section
{
printf("thread num:%d\n",omp_get_thread_num());
fflush(stdout);
}
#pragma omp section
{
printf("thread num:%d\n",omp_get_thread_num());
fflush(stdout);
}
#pragma omp section
{
printf("thread num:%d\n",omp_get_thread_num());
fflush(stdout);
}
}
/*输出结果
thread num:1
thread num:1
thread num:0
thread num:3
thread num:1
thread num:2
*/
哪个section执行哪个指令完全取决于实现;如果section个数多于线程数,每个section都将会被执行,但是如果section数少于线程数,有些线程就不会执行任何操作。如果omp后面不加parallel,默认section是按照串行的顺序执行的,只有加了parallel,才会按照并行的方式执行section。
single
single指令效果是让一条代码块的语句只由一个线程来处理。
void func(){
printf("hello");
//cout<<"hello"<<endl;
}
#pragma omp single
{
func();
}
/*
输出结果(只由一个线程执行一次):
hello
*/
master
master指令表示只能由主线程来执行,其他线程不能执行该代码块的指令。
#pragma omp parallel
{
#pragma omp master
{
for(int i=0;i<10;i++){
cout<<"thread num is:"<<omp_get_thread_num()<<",i:"<<i<<endl;
}
}
}
/*
可以看出来,只有master线程执行了我们的for循环,输出有序。
输出结果:
thread num is:0,i:0
thread num is:0,i:1
thread num is:0,i:2
thread num is:0,i:3
thread num is:0,i:4
thread num is:0,i:5
thread num is:0,i:6
thread num is:0,i:7
thread num is:0,i:8
thread num is:0,i:9
*/
critical
critical指令表明该指令包裹的代码块只能由一个线程来执行,不能被多个线程同时执行。注意,如果多个线程试图执行同一个critical代码块的时候,其他线程会被堵塞,也就是排队等着,直到上一线程完成了代码块的操作,下一个线程才能继续执行这一代码块。
与single不同的是,single只能执行一次,并且是单线程执行,执行完了就不会再执行了,而critical可以多次执行。
int sum = 0;
#pragma omp parallel for shared(sum)
for(int i=0;i<10;i++){
#pragma omp critical
{
printf("thread:%d,sum:%d\n",omp_get_thread_num(),sum);
sum++;
}
}
cout<<sum<<endl;
/*输出结果
thread:0,sum:0
thread:0,sum:1
thread:0,sum:2
thread:2,sum:3
thread:2,sum:4
thread:1,sum:5
thread:1,sum:6
thread:1,sum:7
thread:3,sum:8
thread:3,sum:9
10
*/
从句
可以通过参数private将变量变成每个线程私有的变量,这样,其他线程对该变量的修改不会影响其他线程的变量。这里空说可能不理解,但是可以通过下面的例子来理解。先来看看什么是循环依赖。
依赖(循环依赖)
#include <iostream>
#include <omp.h>
using namespace std;
int main(){
int fib[1000];
fib[0] = 1;
fib[1] = 1;
#pragma omp parallel for
for(int i=2;i<20;i++){
fib[i] = fib[i-2] +fib[i-1];
}
for(int i=0;i<20;i++){
cout<<i<<":"<<fib[i]<<endl;
}
return 0;
}
/*
输出结果:
0:1
1:1
2:2
3:3
4:5
5:8
6:13
7:6
8:6
9:12
10:18
11:30
12:0
13:0
14:0
15:0
16:0
17:0
18:0
19:0
*/
可以看到,后面很多数据的结果都是0,还有很多算的是错的(7之后的数据),为什么呢?因为这里有循环依赖。循环依赖指的是不同线程之间有变量之间的依赖,这个例子中,由于线程之间有变量之间的依赖,所以必须要前面的线程算完了,后面的线程再在前面线程算的结果的基础上来算才能算出正确的结果,有些时候呢,我们可以通过更改逻辑来避免循环依赖,从而使循环在并行下也可以得到计算,并且不会算错。给个例子(这个例子是网上找的):
double factor = 1;
double sum = 0.0;
int n = 1000;
#pragma omp parallel for reduction(+:sum)
for (int i = 0; i < n; i++)
{
sum += factor / (2 * i + 1);
factor = -factor;
}
double pi_approx = 4.0*sum;
cout<<pi_approx<<endl;
//上面的例子会输出错误的值,这是因为不同线程之间有循环依赖
//消除循环依赖后
#pragma omp parallel for reduction(+:sum)
for (int i = 0; i < n; i++)
{
if(i%2){
factor = 1;
}else{
factor = -1;
}
sum += factor / (2 * i + 1);
}
double pi_approx = 4.0*sum;
cout<<pi_approx<<endl;
/*这下消除了循环依赖,但结果其实依然不正确,
这是因为不同的线程之间的factor其实还是shared的,
这样一个线程给shared赋值的时候可能会影响到其他线程对
sum求和时读取factor变量的错误,因此我们只需要再改一步,
让factor变为private即可。
*/
#pragma omp parallel for reduction(+:sum) private(factor)
schedule
schedule是对循环并行控制中的任务调度的指令,例如,我们在写parallel for的时候,我们开n个线程去并行执行一段for循环代码,但是我们怎么知道哪个线程执行哪次迭代的代码呢?我们能不能控制呢?schedule就是做这个工作的。parallel for下我们默认是通过static进行调度的,每个线程分配chunk_size个迭代任务,而这个chunk_size是通过迭代数除以线程数算出来的(最后一个线程可能分配的少一些)。
type
- static:静态分配任务,指的是分配的过程跟运行的时候任务分配无关
- dynamic:动态调度,不指定size,则按照谁先执行完任务下一次迭代就分配给谁,无法提前预知哪个线程执行哪次迭代。而指定size之后,表示每次执行完任务之后分配给一个线程的迭代任务数量。
- guided:
- runtime:根据环境变量确定是上面调度策略中的哪种。
#pragma omp parallel schedule(type,size)
//这里type可以选择static、dynamic、guided、runtime
omp_set_num_threads(4);
//int sum =0;
#pragma omp parallel for schedule(static,4)
for(int i=0;i<16;i++){
//sum+=i;
printf("thread is:%d,thread sum is:%d\n",omp_get_thread_num(),i);
fflush(stdout);
}
/*
输出
thread is:2,thread sum is:8
thread is:2,thread sum is:9
thread is:1,thread sum is:4
thread is:1,thread sum is:5
thread is:1,thread sum is:6
thread is:1,thread sum is:7
thread is:2,thread sum is:10
thread is:2,thread sum is:11
thread is:3,thread sum is:12
thread is:3,thread sum is:13
thread is:3,thread sum is:14
thread is:3,thread sum is:15
thread is:0,thread sum is:0
thread is:0,thread sum is:1
thread is:0,thread sum is:2
thread is:0,thread sum is:3
*/
//dynamic
omp_set_num_threads(4);
//int sum =0;
#pragma omp parallel for schedule(dynamic,4)
for(int i=0;i<16;i++){
//sum+=i;
printf("thread is:%d,thread sum is:%d\n",omp_get_thread_num(),i);
fflush(stdout);
}
/*输出
thread is:2,thread sum is:4
thread is:2,thread sum is:5
thread is:3,thread sum is:12
thread is:2,thread sum is:6
thread is:2,thread sum is:7
thread is:0,thread sum is:0
thread is:0,thread sum is:1
thread is:0,thread sum is:2
thread is:0,thread sum is:3
thread is:3,thread sum is:13
thread is:3,thread sum is:14
thread is:3,thread sum is:15
thread is:1,thread sum is:8
thread is:1,thread sum is:9
thread is:1,thread sum is:10
thread is:1,thread sum is:11
*/
从以上两个输出呀我们可以看出,static的线程id和输出的值我们是可以预知的,但是dynamic的输出值和线程id不是对应的,多次输出可以看出来,是动态分配的。
ordered
ordered指令顾名思义,可以让循环代码中的某些代码执行按照一定的顺序来执行。下面的实例告诉我们了ordered是怎么工作的。
#pragma omp parallel for ordered
for(int i=0;i<20;i++){
//#pragma omp ordered
{
cout<<"thread num is:"<<omp_get_thread_num();
}
cout<<",i:"<<i<<endl;
}
/*
可以看出ordered下for循环按照顺序执行,线程也是按照顺序执行,等价于用不同的线程去串行执行一段代码
输出结果:
thread num is:0,i:0
thread num is:0,i:1
thread num is:0,i:2
thread num is:1,i:3
thread num is:1,i:4
thread num is:1,i:5
thread num is:2,i:6
thread num is:2,i:7
thread num is:2,i:8
thread num is:3,i:9
thread num is:3,i:10
thread num is:3,i:11
thread num is:4,i:12
thread num is:4,i:13
thread num is:5,i:14
thread num is:5,i:15
thread num is:6,i:16
thread num is:6,i:17
thread num is:7,i:18
thread num is:7,i:19
*/
private
表明某个变量是私有的
#pragma omp parallel private(name)
//代码是循环依赖的代码
上面有跑例子的
firstprivate
表明某个变量线程私有,但是在进入线程之前先给私有变量赋全局变量的初值
//用法
#pragma omp parallel firstprivate(name)
//例子
int sum = 0;
#pragma omp parallel for firstprivate(sum)
for(int i=0;i<20;i++){
sum+=i;
printf("thread num is:%d,sum is:%d\n",omp_get_thread_num(),sum);
//cout<<"thread num is:"<<omp_get_thread_num()<<",sum is:"<<sum<<endl;
}
cout<<"real sum is:"<<sum<<endl;
/*下面是输出结果,这里可以看出不同线程计算得到的sum是不一样的,
并且不影响全局变量sum 的值。sum在最开始都初始化为0,输出的是加上i之后的值。
*/
/*
thread num is:0,sum is:0
thread num is:0,sum is:1
thread num is:0,sum is:3
thread num is:0,sum is:6
thread num is:0,sum is:10
thread num is:3,sum is:15
thread num is:3,sum is:31
thread num is:3,sum is:48
thread num is:3,sum is:66
thread num is:3,sum is:85
thread num is:2,sum is:10
thread num is:2,sum is:21
thread num is:2,sum is:33
thread num is:2,sum is:46
thread num is:1,sum is:5
thread num is:1,sum is:11
thread num is:1,sum is:18
thread num is:2,sum is:60
thread num is:1,sum is:26
thread num is:1,sum is:35
real sum is:0
*/
lastprivate
表明某个变量线程私有,但是在线程结束之后将最后一个section的值赋给全局变量,本来也没闹清楚,后来实验了一下,大致清楚了,因为不同section之间计算的结果不一样嘛,就是最后执行完运算的那个section把值赋给全局变量
#pragma omp parallel lastprivate(name)
/*
下面是具体实验
可以看到,全局的sum输出的其实是thread3最后的结果,因为thread3到最后执行的时候sum=8,i=9,加起来就是17了,所以在最后取最后一个thread计算的结果赋值给全局变量sum。
*/
int sum = 0;
#pragma omp parallel for lastprivate(sum)
for(int i=0;i<10;i++){
printf("thread:%d,sum:%d\n",omp_get_thread_num(),sum);
sum +=i;
}
cout<<sum<<endl;
/*
thread:2,sum:0
thread:2,sum:6
thread:1,sum:0
thread:1,sum:3
thread:1,sum:7
thread:3,sum:0
thread:3,sum:8
thread:0,sum:32767
thread:0,sum:32767
thread:0,sum:32768
17*/
shared
表明某个变量是线程之间共享的.
#pragma omp parallel shared(name)
//这个默认就是shared的,不需要代码实验
default
要么是shared,要么是private.
#pragma omp parallel default(shared)
//一样不需要代码实验
reduction
通过operator规约,避免数据竞争,表明某个变量私有,在线程结束后加到全局变量上去,reduction每个线程创建变量的副本,按照operator进行初始化值,然后在线程结束的时候都加到全局变量之上。
#pragma omp parallel reduction(operation:name)
//实验在上面循环依赖的代码那里
int res = 10;
#pragma omp parallel for reduction(+:res)
for(int i=0;i<10000;i++){
res = res + i;
}
/*
这个程序会创建n个线程(默认取决与你的cpu数量),
每个线程里的res被初始化为0并且线程私有,
当每个线程都计算完成之后,将自己线程内的res值加到全局的res上去。
最后跟不parallel的结果一样,但是实现过程不一样。
*/
在循环次数比较多的情况下,会发生数据竞争(因为默认在并行体外定义的变量是shared的,在里面定义的是private的,shared的数据会由于在并行体内被修改而影响其他线程的赋值,所以会发生数据竞争),所以通过reduction规约,可以避免这种情况发生。
operator对应的初始化值的表(表格是网上找的):
操作 | 操作符 | 初始值 |
---|---|---|
加法 | + | 0 |
乘法 | * | 1 |
减法 | - | 0 |
逻辑与 | && | 0 |
逻辑或 | || | 0 |
按位与 | & | 1 |
按位或 | | | 0 |
按位异或 | ^ | 0 |
相等 | true | |
不等 | false | |
最大值 | max | 最小负值 |
最小值 | min | 最大正值 |
实验部分
由于我随便跑了一跑高斯模糊算法的手动实现(之前用python)写过,这次由于学了OpenMP这个并行编程的东西,打算跑一跑看看到底能提高多快(我的算法是最原始的算法),我的算法需要有opencv库(虽然opencv中已经有GaussianBLur函数,而且速度很快),但我主要想弄清楚高斯模糊算法到底怎么回事才用这么复杂的矩阵计算做的。
在qt下需要在pro文件中加入以下配置,这里由于我用到了opencv,所以还需要加入这样的配置:
QMAKE_CXXFLAGS+= -fopenmp
LIBS+= -lgomp -lpthread
INCLUDEPATH+=/usr/local/include\
/usr/local/include/opencv\
/usr/local/include/opencv2
LIBS+=/usr/local/lib/libopencv_highgui.so\
/usr/local/lib/libopencv_core.so\
/usr/local/lib/libopencv_imgproc.so\
/usr/local/lib/libopencv_imgcodecs.so
下面是parallel和不parallel的结果对比:
#include "mainwindow.h"
#include <QApplication>
#include "opencv2/opencv.hpp"
#include <omp.h>
#include <vector>
#include <time.h>
#include <math.h>
#define PI 3.1415926
using namespace cv;
using namespace std;
Mat getWeight(int r=3){
int l = r*2+1;
float sum=0;
Mat temp(l,l,CV_32FC1);
//
#pragma omp parallel for reduction(+:sum)
for(int i=0;i<temp.rows;i++){
for(int j=0;j<temp.cols;j++){
temp.at<float>(i,j) = 0.5/(PI*r*r)*exp(-((i-l/2.0)*(i-l/2.0) + (j-l/2.0)*(j-l/2.0))/2.0/double(r)/double(r));
sum = sum+ temp.at<float>(i,j);
//cout<<sum<<endl;
}
}
return temp/sum;
}
float Matrix_sum(Mat src){
float temp=0;
for(int i=0;i<src.rows;i++){
for(int j=0;j<src.cols;j++){
temp +=src.at<float>(i,j);
}
}
return temp;
}
Mat GaussianBlur_parallel(Mat src,Mat weight,int r=3){
Mat out(src.size(),CV_32FC1);
Mat temp;
int rows = src.rows-r;//shared
int cols = src.cols-r;//shared
#pragma omp parallel for
for(int i=r;i<rows;i++){
for(int j=r;j<cols;j++){
Mat temp(src,Range(i-r,i+r+1),Range(j-r,j+r+1));//int to float32
temp.convertTo(temp,CV_32FC1);
out.at<float>(i,j) = Matrix_sum(temp.mul(weight));
//cout<<"thread num is:"<<omp_get_num_threads()<<endl;
}
}
out.convertTo(out,CV_8U);
return out;
}
Mat GaussianBlur_normal(Mat src,Mat weight,int r=3){
Mat out(src.size(),CV_32FC1);
for(int i=r;i<src.rows-r;i++){
for(int j=r;j<src.cols-r;j++){
Mat temp(src,Range(i-r,i+r+1),Range(j-r,j+r+1));
temp.convertTo(temp,CV_32FC1);
//cout<<Matrix_sum(temp.mul(weight))<<endl;
//cout<<"("<<i-r<<","<<i+r+1<<")"<<","<<"("<<j-r<<","<<j+r+1<<")"<<"raw:"<<src.size()<<endl;
out.at<float>(i,j) = Matrix_sum(temp.mul(weight));
}
}
out.convertTo(out,CV_8U);
return out;
}
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
omp_set_num_threads(8);
vector<Mat> split_img,out_img;
Mat src = imread("/home/xueaoru/projects/srm/car.jpg"),normal_img,parallel_img;
Mat weight = getWeight();
split(src,split_img);
out_img.clear();
double start_time = (double)getTickCount(),end_time;
for(auto s:split_img){
out_img.push_back(GaussianBlur_normal(s,weight));
}
end_time = (double)getTickCount();
cout<<"Normal compute time:"<<(end_time-start_time)*1000/getTickFrequency()<<"ms"<<endl;
merge(out_img,normal_img);
imshow("normal GaussianBlur",normal_img);
out_img.clear();
start_time = (double)getTickCount();
for(auto s:split_img){
out_img.push_back(GaussianBlur_parallel(s,weight));
}
end_time = (double)getTickCount();
cout<<"parallel compute time:"<<(end_time-start_time)*1000/getTickFrequency()<<"ms"<<endl;
merge(out_img,parallel_img);
imshow("parallel GaussianBlur",parallel_img);
imshow("raw",src);
return a.exec();
}
/*
输出结果如下:
Normal compute time:1609.72ms
parallel compute time:871.309ms
*/