OpenMP学习 第六章 OpenMP数据环境
第六章 OpenMP数据环境
数据环境
线程是一个执行实体.它执行程序中的语句,并修改存储在内存中的项.
在内存中,一个项驻留在一个指定的地址.我们为这个地址指定一个名称,并称其为变量.
OpenMP程序使用并行构造来创建一个线程组.所有在并行构造内执行的代码称为 并行区域(parallel region)
仅仅使用一个并行构造,所有线程执行相同的代码,这类算法被称为 SPMD模式.
通常而言,我们把线程执行一个区域时可见的变量集称为该区域的 数据环境.
OpenMP线程可以访问共享的地址空间和该线程私有的地址空间.因此,OpenMP通用核心中的变量可以被分配为两种存储属性之一: 私有 或 共享.
缺省存储属性
当程序被启动时,操作系统会创建一个进程来运行程序.该进程包含一个或多个线程,线程可见的内存块(通常为 堆 形式)及与系统交互所需要的其他资源.
变量从进程管理的堆内存中开始.他们对所有的线程都是可见的,因而可以说是 共享的.
一个进程fork出一组线程.每个线程创建时都有自己的程序计数器,记忆程序本地的内存块(通常为 栈 形式).线程栈中的变量只有该线程能看到,因此可以说是 私有的.
基础语言中全局范围的变量在线程之间是共享的.例如 用static声明的变量 和 通过malloc或new动态分配内存中的堆变量 是共享的.
一个简单的思路是: 进程堆中分配的变量是共享的,而线程栈上的变量是私有的.
修改存储属性
通常而言,一个线程遇到一个OpenMP构造并创建一个新的区域.区域有一个相关的数据环境.数据环境子句将遇到的线程数据环境中的变量映射到刚刚创建的新区域的数据环境中.
这些子句设置了变量的数据共享属性.
- 设置变量数据共享属性的子句:
#pragma omp parallel shared(list)
- 设置变量数据私有属性的子句(未初始化):
#pragma omp parallel private(list)
- 设置变量数据私有属性的子句(初始化):
#pragma omp parallel firstprivate(list)
- 设置强制列出变量存储属性的子句:
#pragma omp parallel default(none)
其中list是一个逗号分割的变量列表.
通常而言,shared子句可以不需要,但是为了调试和阅读程序,将共享变量与shared子句一起列出是个好习惯.
private子句则为每个线程创建一个类型和名称相同的新变量, 其中私有变量的值时未初始化的.
firstprivate子句与private子句相似, 但是其中私有变量的值是初始化了的.
default(none)子句强制要求明确所有传递到区域中的变量都必须明确地列出在shared,private,firstprivate,reduction等子句中.
曼德勃罗集的面积
问题描述:
曼德勃罗特集是一个几何图形,曾被称为"上帝的指纹".这个点集均出自公式:
$$Z_n+1=(Z_n)^2+C$$
当对一个函数迭代计算时,这些点将处于准稳定状态(quasi-stable),通常该函数为:
$$Z_{k+1}=Z_k+C$$
式子中$Z_{k+1}$是复数$Z=a+bi$的第$k+1$次迭代,$Z$初始值为0,$C$为决定点位置的复数.
化简运算:
$$Z2=(a2-b^2)+(2ab)i$$
在这里,我们简单选择一片grid,其中$r\in[-2.0,0.5)$,$i\in[-1.125,1.125]$,
然后对所有点进行检查,通过数点的数量对其面积进行估算.
易知,曼德勃罗集图形关于x轴对称,上面grid的面积约为$1.506$.
下面是该思路的代码实现:
#include <iostream>
#include <vector>
#include <omp.h>
import <format>;
#define NPOINTS 1000
#define MAXITER 1000
#define TURNS 10
typedef struct d_complex {
double r;
double i;
}d_complex;
d_complex c;
int num_outside = 0;
void testPoint(d_complex c)
{
d_complex z;
double temp;
z = c;
for (int i = 0; i < MAXITER; i++) {
temp = (z.r * z.r) - (z.i * z.i) + c.r;
z.i = 2 * z.r * z.i + c.i;
z.r = temp;
if ((z.r * z.r + z.i * z.i) > 4.0) {
#pragma omp critical
num_outside++;
break;
}
}
return;
}
int main()
{
std::vector<std::pair<double, double>>result;
std::vector<double>times;
for (int iter = 0; iter < TURNS; iter++) {
double area, err, eps = 1.0e-5;
double temp_time, run_time;
num_outside = 0;
temp_time = omp_get_wtime();
#pragma omp parallel
{
int j;
#pragma omp for private(c,j) firstprivate(eps) collapse(2) nowait
for (int i = 0; i < NPOINTS; i++)
for (j = 0; j < NPOINTS; j++) {
c.r = -2.0 + 2.5 * (double)(i) / (double)(NPOINTS)+eps;
c.i = 1.125 * (double)(j) / (double)(NPOINTS)+eps;
testPoint(c);
}
}
area = 2.0 * 2.5 * 1.125 * (double)(NPOINTS * NPOINTS - num_outside) / (NPOINTS * NPOINTS);
err = area / (double)NPOINTS;
run_time = omp_get_wtime() - temp_time;
result.push_back(std::make_pair(area,err));
times.push_back(run_time);
}
for (int iter = 0; iter < times.size(); iter++) {
std::cout << std::format("in the {} answer,answer is {:.15f} +/- {:.15f},time is {:.15f}",
iter,
result[iter].first,
result[iter].second,
times[iter]
) << std::endl;
}
return 0;
}
运行发现结果为$1.512118125$,接近于$1.506$,验证了结果.
重新审视Pi循环
回到前面第五章内容所提到的带有并行循环共享工作的Pi循环:https://www.cnblogs.com/mesonoxian/p/17971593
#include <iostream>
#include <fstream>
#include <omp.h>
import <format>;
#define TURNS 100
#define PI 3.141592653589793
long double num_steps = 1e8;
double step;
int main()
{
std::ofstream out;
out.open("example.csv", std::ios::ate);
out << "NTHREADS,pi,err,run_time,num_steps" << std::endl;
double sum = 0.0;
for (int NTHREADS = 1; NTHREADS < TURNS; NTHREADS++) {
double start_time, run_time;
double pi, err;
pi = sum = 0.0;
int actual_nthreads;
step = 1.0 / (double)num_steps;
omp_set_num_threads(NTHREADS);
start_time = omp_get_wtime();
actual_nthreads = omp_get_num_threads();
#pragma omp parallel
{
double x;
#pragma ompr for reduction(+:sum)
for (int i = 0; i < num_steps; i++) {
x = (i + 0.5) * step;
sum += 4.0 / (1.0 + x * x);
}
}//end of parallel
pi = step * sum;
err = pi - PI;
run_time = omp_get_wtime() - start_time;
std::cout << std::format("pi is {} in {} seconds {} thrds.step is {},err is {}",
pi,
run_time,
actual_nthreads,
step,
err
) << std::endl;
out << std::format("{},{:.15f},{:.15f},{:.15f},{}",
NTHREADS,
pi,
err,
run_time,
num_steps
) << std::endl;
}
out.close();
return 0;
}
我们发现,在其中为每个线程都声明了一个私有的x变量
结合本章所学的知识,我们可以在此处使用private子句,最大限度地减少串行代码转换为OpenMP并行程序所需要的修改量.
#include <iostream>
#include <fstream>
#include <omp.h>
import <format>;
#define TURNS 100
#define PI 3.141592653589793
long int num_steps = 1e8;
double step;
int main()
{
std::ofstream out;
out.open("example.csv", std::ios::ate);
out << "NTHREADS,pi,err,run_time,num_steps" << std::endl;
double sum = 0.0;
for (int NTHREADS = 1; NTHREADS < TURNS; NTHREADS++) {
double start_time, run_time;
double x, pi, err;
pi = sum = 0.0;
int actual_nthreads;
step = 1.0 / (double)num_steps;
omp_set_num_threads(NTHREADS);
start_time = omp_get_wtime();
actual_nthreads = omp_get_num_threads();
#pragma omp parallel for private(x) reduction(+:sum)
for (int i = 0; i < num_steps; i++) {
x = (i + 0.5) * step;
sum += 4.0 / (1.0 + x * x);
}
pi = step * sum;
err = pi - PI;
run_time = omp_get_wtime() - start_time;
std::cout << std::format("pi is {} in {} seconds {} thrds.step is {},err is {}",
pi,
run_time,
actual_nthreads,
step,
err
) << std::endl;
out << std::format("{},{:.15f},{:.15f},{:.15f},{}",
NTHREADS,
pi,
err,
run_time,
num_steps
) << std::endl;
}
out.close();
return 0;
}
上面的代码结合了并行共享工作循环与归约,并且通过private子句减少了转化为并行程序所需要的工作量.
数组与指针
前面我们提到过:
- 进程堆中分配的变量是共享的.
- 线程栈上的变量是私有的.
因而在处理 静态数组 时,为了传递一个静态数组,我们传递其首地址指针即可.
int varr[1000];
#pragma omp parallel private(varr)
{
//body of loop
}
而在处理 动态数组 及 指针 时,我们需要考虑其存储位置.
OpenMP提供了一系列数组区段来帮助我们让每个线程分配并复制一个原来是数组的变量到并行区域:
[begin:length:step-length]
[begin:length]//默认步长为1
[:length:step-length]//默认初始位置为0
例如下面这样:
int* varr = new int[1000];
#pragma omp parallel firstprivate(varr[0:1000:1])
{
//body of loop
}