Data-Sharing Rules
变量在OpenMP中可以是共享(shared)还是私有(private)的,共享和私有被称作变量Data-sharing的属性(attribute)。如果变量是共享的,在所有线程中存在一个实例使得这个变量被共享。如果变量是私有的,每个线程将会得到私有变量的local copy。
Implicit Rules
OpenMP有一系列规则,来推测数据共享的属性(attributes)。
例如,考虑下面这段代码:
int i = 0; int n = 10; int a = 7; #pragma omp parallel for for (i = 0; i < n; i++) { int b = a + i; ... }
有4个变量i
,n
,a
,b
。
在并行区域以外声明变量的数据共享属性通常是共享的,n
,a
是共享变量
对于循环迭代变量,默认是私有的,因此i
是私有变量。
在并行区域内部声明的变量是私有的,b
是私有的。
这里建议声明在循环内部声明循环变量,这样,变量私有就会非常明确。
int n = 10; // shared int a = 7; // shared #pragma omp parallel for for (int i = 0; i < n; i++) // i private { int b = a + i; // b private ... }
Explicit Rules
我们可以显式设置变量Data-sharing的属性。
Shared
shared(list)
子句声明在list中的变量都是共享的,例子:
#pragma omp parallel for shared(n, a) for (int i = 0; i < n; i++) { int b = a + i; ... }
a
和n
都是共享变量。
注意OpenMP没有给出一个机制来防止共享变量之间的data race。这应该是程序员的责任。
所谓data race就是不同线程同时读入一个变量,不同时写一个变量,会造成结果不对。
例如:
做求和,四个线程需要将求出的结果放在变量sum
里。那么这四个线程如果同时读写sum可能会出错(四个线程同时读到一个老值,得到的新值将仅仅是某一个线程的结果加到sum里)。具体来说: 设sum=6,线程1得到1,线程2得到2,线程3得到3,线程4得到4, 四个线程现在想要将算出来的结果同时写到sum里, 假设四个线程运行的求和代码为sum = sum + a;
其中a为每个线程求得的值 此时等式右边的sum为6,所以对于线程1来说,现在要运行的就是sum = 6 + 1
,写入得新sum=7, 但是线程2在线程1写入前,就已经读到sum=6,那么对于线程2,运行的就是sum = 6 + 2
,得到sum=8. 其他两个依次类推, 所以最终得到的值是什么不确定。
共享变量会引入额外开销,因为变量的一个实例在多个线程之间共享。当需要良好性能时候,应该尽量减少共享变量的数量。
Private
private(list)
子句声明在list中的变量都是私有的
#pragma omp parallel for shared(n, a) private(b) for (int i = 0; i < n; i++) { b = a + i; ... }
这里变量b
是私有变量。每个线程都有变量b
的local copy。
私有变量有时候是反直觉的。假设私有变量在并行区域前面已经被声明,但是在并行区域开始阶段,这个私有变量值变为未定义,在并行区域结束以后,也会变成未定义。例如:
int p = 0; // the value of p is 0 #pragma omp parallel private(p) { // the value of p is undefined p = omp_get_thread_num(); // the value of p is defined ... } // the value of p is undefined
所以为了顺从我们的直觉,我们尽量在并行区域内部定义变量。例如上面这段代码,我们可以变成
#pragma omp parallel { int p = omp_get_thread_num(); ... }
这样的写法也能提高代码可读性。
Default
default有两个版本。我们先来看一下default(shared)
default(shared)
default(shared)
子句将会把所有涉及到数据共享的变量设置为共享变量。例如:
int a, b, c, n; ... #pragma omp parallel for default(shared) for (int i = 0; i < n; i++) { // using a, b, c }
这里a
,b
,c
,n
都是共享变量
另外也可以将大多数变量默认为shared,将部分变量特指成private。
int a, b, c, n; #pragma omp parallel for default(shared) private(a, b) for (int i = 0; i < n; i++) { // a and b are private variables // c and n are shared variables }
Default(none)
default(none)
子句强制程序员指定所有变量的data sharing属性。
人在烦的时候,可能会瞎写出这些代码:
int n = 10; std::vector<int> vector(n); int a = 10; #pragma omp parallel for default(none) shared(n, vector) for (int i = 0; i < n; i++) { vector[i] = i * a; }
然后编译器就会报错:
error: ‘a’ not specified in enclosing parallel
vector[i] = i * a;
^
error: enclosing parallel
#pragma omp parallel for default(none) shared(n, vector)
编译器吃出这里a没有被标明数据共享属性,修改成:
int n = 10; std::vector<int> vector(n); int a = 10; #pragma omp parallel for default(none) shared(n, vector, a) for (int i = 0; i < n; i++) { vector[i] = i * a; }
可以通过编译。
写在最后:
有两条规则
- 鼓励大家在写并行区域时候,使用default(none)子句,这样可以迫使程序员思考变量的数据类型应该是怎样的
- 尽可能在并行区域内声明私有变量,提高代码可读性,使得逻辑清晰
Links: