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:
