0%

Cpp OpenMP

很少截图,因为懒。

hello world

1
2
3
4
5
6
7
#include <iostream>
int main()
{
#pragma omp parallel
std::cout<<"hello world"<<std::endl;
return 0;
}

编译:
g++ -o2 -std=c++14 -fopenmp hellp_world.cpp -o hello_world
-02 哦二不是零二:
-O0
-O1
-O2
-O3
编译器的优化选项的4个级别,-O0表示没有优化,-O1为缺省值,-O3优化级别最高
运行:

  • `./hello_world’
  • OPM_NUM_THREADS=2 ./hellp_world设置环境变量,控制可以使用的线程数。
    结果:
    Parallel_omp_helloworld

库函数

需要#include <omp.h>

#pragma omp parallel num_threads(4)设置生成的线程数量,这个应该不算库函数。
| 函数 |功能和作用 | 示例 |
| ——- | ——- | —— |
|omp_get_thread_num() | 获得当前这个线程的编号id | int i = omp_get_thread_num(); |
|omp_get_num_threads() | 获得当前线程总数 | int n = omp_get_num_threads();|
| omp_set_num_threads(); | 在串行部分设置线程个数,优先级比环境变量高 |omp_set_num_threads(6); |
|omp_get_max_threads() | 这是什么东西?? | .. |

线程数设置的优先级:指导语句的num_threads(),库函数omp_set_num_threads();环境变量;编译器默认。

parallel for

parallel_for

隐含同步

隐含同步
#pragra omp parallel for写成

1
2
3
4
5
#pragra omp parallel
{
##pragra omp for
...
}

前后数据、工作有关系的情况下。比如锁,数据的运算一定是在初始化之后,那之前参与初始化的线程肯定要同时结束才行。
在一些不需要隐含同步的地方,可以用nowait,即#pragma omp for nowait
nowait

变量共享和私有化

private

1
2
3
4
5
6
7
8
9
10
11
/*变量共享和私有化*/
#include<iostream>
#include<omp.h>
int main()
{
#pragma omp parallel for num_threads(4)
for (int i = 0; i < 4; i++)
for (int j = 0; j < 4; j++)
printf("id:%d %d %d\n", omp_get_thread_num(), i, j);
return 0;
}

输出:
共享变量和私有化1

将i和j放在外面

1
2
3
4
5
6
7
8
9
10
11
12
#include<iostream>
#include<omp.h>
int main()
{
int i = 0;
int j = 0;
#pragma omp parallel for num_threads(4)
for ( i = 0; i < 4; i++)
for ( j = 0; j < 4; j++)
printf("id:%d %d %d\n", omp_get_thread_num(), i, j);
return 0;
}

结果:
共享变量和私有化2

加入private

1
2
3
4
5
6
7
8
9
10
11
12
#include<iostream>
#include<omp.h>
int main()
{
int i = 0;
int j = 0;
#pragma omp parallel for private(j) num_threads(4)
for ( i = 0; i < 4; i++)
for ( j = 0; j < 4; j++)
printf("id:%d %d %d\n", omp_get_thread_num(), i, j);
return 0;
}

之前在循环外先声明,那不同线程循环变量就会访问同一块内存,这样就会冲突。加入private(j)之后就会在每个线程生成一个副本,这样就正常了。

firstprivate

使用firstprivate(j)不仅有全局变量的副本,而且有他的值,在并行部分里对他进行修改不会影响全局变量的值。

捕捉私有变量

用数组的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<iostream>
#include<omp.h>
int main()
{
const int num = omp_get_max_threads();
int* aux = new int[num];
int i = 1;
#pragma omp parallel firstprivate(i)
{
const int j = omp_get_thread_num();
i += j;
aux[j] = i;
}
for (int i = 0; i < num; i++)
printf("aux[% d] = % d\n", i, aux[i]);
delete[] aux;
return 0;
}

运行结果
捕捉私有变量1

lastprivate

1
2
3
4
5
6
7
8
9
10
11
12
#include<iostream>
#include<omp.h>
int main()
{
int i = 0;
#pragma omp parallel for lastprivate(i) num_threads(4)
for (int j = 0; j < 4; j++)
i = j;

printf("%d\n", i);
return 0;
}

可以返回变量的值。
输出结果是3.

改为:

1
2
3
#pragma omp parallel for  lastprivate(i)  num_threads(4) 
for (int j = 0; j < 4; j++)
i = j + omp_get_thread_num();

输出结果是6.

DMV

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#include <iostream>
#include <cstdint>
#include <vector>

// hpc_helpers contains the TIMERSTART and TIMERSTOP macros
// and the no_init_t template that disables implicit type
// initialization
#include "../include/hpc_helpers.hpp"

template <typename value_t,
typename index_t>
void init(std::vector<value_t>& A,
std::vector<value_t>& x,
index_t m,
index_t n) {

for (index_t row = 0; row < m; row++)
for (index_t col = 0; col < n; col++)
A[row*n+col] = row >= col ? 1 : 0;

for (index_t col = 0; col < m; col++)
x[col] = col;
}

template <typename value_t,
typename index_t>
void mult(std::vector<value_t>& A,
std::vector<value_t>& x,
std::vector<value_t>& b,
index_t m,
index_t n,
bool parallel) {

#pragma omp parallel for if(parallel)
for (index_t row = 0; row < m; row++) {
value_t accum = value_t(0);
for (index_t col = 0; col < n; col++)
accum += A[row*n+col]*x[col];
b[row] = accum;
}
}

int main() {
const uint64_t n = 1UL << 15;
const uint64_t m = 1UL << 15;

TIMERSTART(overall)
// memory allocation for the three vectors x, y, and z
// with the no_init_t template as a wrapper for the actual type
TIMERSTART(alloc)
std::vector<no_init_t<uint64_t>> A(m*n);
std::vector<no_init_t<uint64_t>> x(n);
std::vector<no_init_t<uint64_t>> b(m);
TIMERSTOP(alloc)

// manually initialize the input matrix A and vector x
TIMERSTART(init)
init(A, x, m, n);
TIMERSTOP(init)

// compute A * x = b sequentially three times
for (uint64_t k = 0; k < 3; k++) {
TIMERSTART(mult_seq)
mult(A, x, b, m, n, false);
TIMERSTOP(mult_seq)
}
// compute A * x = b in parallel three times
for (uint64_t k = 0; k < 3; k++) {
TIMERSTART(mult_par)
mult(A, x, b, m, n, true);
TIMERSTOP(mult_par)
}
TIMERSTOP(overall)

// check if (last) result is correct
for (uint64_t index = 0; index < m; index++)
if (b[index] != index*(index+1)/2)
std::cout << "error at position " << index
<< " " << b[index] << std::endl;
}

运行结果:
自己电脑:
捕捉私有变量1
学校服务器:
omp_DMV_学校服务器
看来是x64不行。我改回了x86,然后量改成了13次方,结果是这样:
omp_DMV_自己电脑x86

collapse

1
2
3
4
5
6
7
8
9
10
11
12
13
#pragma omp parallel for collapse(2) if(parallel)
for (index_t i = 0; i < num_test; i++)
for (index_t j = 0; j < num_train; j++) {
value_t accum = value_t(0);
// fine-grained parallelism
// #pragma omp parallel for reduction(+:accum)
for (index_t k = 0; k < num_features; k++) {
const value_t residue = test [i*num_features+k]
- train[j*num_features+k];
accum += residue*residue;
}
delta[i*num_train+j] = accum;
}

collapse折叠。因为这里钱两层循环紧挨着,所以可以“折叠起来”,超索引地给不同的线程分配任务。
有的时候,外层循环不多,但可用的线程非常你多的时候,可以用这种方法提高资源利用率。(解决负载不均衡。)

critical

omp_critical

就相当于锁。

1NN(最近邻)

不平衡调度

是来研究parallel for怎么调度的。
格式:schedule(type[,size])type可选有static , dynamic , guided ,runtimesize是个整数,前三种的时候,可写可不写,rumtime无需写很具环境变量来。

static

没有schedule,默认是static

1
2
3
4
5
6
7
8
9
#include<iostream>
#include<omp.h>
int main()
{
#pragma omp parallel for schedule(static) num_threads(4)
for (int i = 0; i < 10; i++)
printf("i=%d,thread_id=%d\n", i, omp_get_thread_num());
return 0;
}

如果带了参数size,那它就会按线程编号0、1、2..的顺序,每个线程分配size个任务,一轮不够重头再来。如果不带参数,那它会让每个线程的数量均衡,比如10个循环4个线程的情况,4个县城依次是3、3、2、2。感觉它是算好怎样分配每个线程任务均衡,然后分配那么多连续的任务。
如果size为1,那就像之前的循环调度;不设参数有点类似于区块分发;参数大于1,就有点类似于块循环了。

dynamic

这种方式运行多次后给我的感觉是:

  • 不带参数和参数为1效果一样;
  • 带了参数之后,按任意编号顺序给每个线程分配size多;
  • 如果一次之后还有没分配完的,那就都给0号线程。
  • 但看mnist的运行结果和老师讲解,可能是谁闲下来谁上。

    guided

    omp_guided

    runtime

    omp_runtime

    auto

    还有一种auto,由编译器和系统一起决定的。

归约

竞争条件。容易理解。不同的线程访问在并行区域外声明的变量并修改。
解决方案一:critical
前面有提到,就相当于锁。
解决方案二:归约reduction
omp_reduction_1_min
x结果是4。
分析:

  1. 首先给每个线程分配任务,在这里因为没有schedule子句,所以按静态调度分,0号线程分前5个,1号线程分后五个;
  2. 每个线程声明一个私有化变量,叫中性变量,并赋予初值,因为这里是求最小值,所以赋值为无符号整型的最大值(64个1);
  3. 对中性变量做5次加1,得到4;
  4. 两个线程的结果和x的初始值取最小值返回给x。

reduction的基本操作:
omp_redution_基本操作

归约的数学性质或自定义归约的要求:

omp_reduction_性质或要求
结合律、交换律、与中性元素运算后还是副本的值。(a,b,c是副本的值)

自定义归约

结构:

1
2
3
#pragma omp declare reduction
(indentifier:typelist:combiner)
[initializer(initializer-expression)]

可以理解为:
1
2
3
#pragma omp declare(自定义的) reduction
(自定义归约名字:类型:参数(或简单的表达式))
[初始化初值]

一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include<iostream>
#include<limits.h>

//最大值返回函数
int mymax(int r, int n)
{
return r > n ? r : n;
}

int main()
{
const int num = 100;
int data[num];
for (int i = 0; i < num; i++)
data[i] = i;

#pragma omp declare reduction \
(rwz:int:omp_out=mymax(omp_out,omp_in))\
initializer(omp_priv=INT_MIN)

int m = INT_MIN;

#pragma omp parallel for reduction(rwz:m)
for (int i = 0; i < num; i++)
{
m = mymax(m, data[i]);
}
std::cout << "max=" << m << std::endl;
return 0;
}



结果是99

绝对值最大
omp_自定义归约绝对值最大

SIMD向量化

1
2
#pragma omp simd
#pragma omp parallel for simd