OpenMP 入门教程

前两天(实际上是几个月之前了)看到了代码中有 #pragma omp parallel for 一段,感受好像是 OpenMP,之前看到并行化的东西都是直接躲开,既然躲不开了,不妨研究一下:ios

OpenMP 是 Open MultiProcessing 的缩写。OpenMP 并非一个简单的函数库,而是一个诸多编译器支持的框架,或者说是协议吧,总之,不须要任何配置,你就能够在 Visual Studio 或者 gcc 中使用它了。程序员

咱们就分三部分来介绍吧,由于我看的那个英文教程就是分了三部分(哈哈) . 如下翻译自英特尔的文档算法

Hello World

把下面的代码保存为 omp.cc数组

#include <iostream>
#include <omp.h>

int main()
{
#pragma omp parallel for
    for (char i = 'a'; i <= 'z'; i++)
        std::cout << i << std::endl;

    return 0;
}

而后 g++ omp.cc -fopenmp就能够了数据结构

入门

循环的并行化

OpenMP的设计们但愿提供一种简单的方式让程序员不须要懂得建立和销毁线程就能写出多线程化程序。为此他们设计了一些pragma,指令和函数来让编译器可以在合适的地方插入线程大多数的循环只须要在for以前插入一个pragma就能够实现并行化。并且,经过把这些恼人的细节都丢给编译器,你能够花费更多的时间来决定哪里须要多线程和优化数据结构多线程

下面个这个例子把32位的RGB颜色转换成8位的灰度数据,你只须要在for以前加上一句pragma就能够实现并行化了负载均衡

#pragma omp parallel for
for (int i = 0; i < pixelCount; i++) {
    grayBitmap[i] = (uint8_t)(rgbBitmap[i].r * 0.229 +
                              rgbBitmap[i].g * 0.587 +
                              rgbBitmap[i].b * 0.114);
}

神奇吧,首先,这个例子使用了“work sharing”,当“work sharing”被用在for循环的时候,每一个循环都被分配到了不一样的线程,而且保证只执行一次。OpenMP决定了多少线程须要被打开,销毁和建立,你须要作的就是告诉OpenMP哪里须要被线程化。框架

OpenMP 对能够多线程化的循环有以下五个要求:函数

  1. 循环的变量变量(就是i)必须是有符号整形,其余的都不行。
  2. 循环的比较条件必须是< <= > >=中的一种
  3. 循环的增量部分必须是增减一个不变的值(即每次循环是不变的)。
  4. 若是比较符号是< <=,那每次循环i应该增长,反之应该减少
  5. 循环必须是没有奇奇怪怪的东西,不能从内部循环跳到外部循环,goto和break只能在循环内部跳转,异常必须在循环内部被捕获。

若是你的循环不符合这些条件,那就只好改写了oop

检测是否支持 OpenMP

#ifndef _OPENMP
    fprintf(stderr, "OpenMP not supported");
#endif

避免数据依赖和竞争

当一个循环知足以上五个条件时,依然可能由于数据依赖而不可以合理的并行化。当两个不一样的迭代之间的数据存在依赖关系时,就会发生这种状况。

// 假设数组已经初始化为1
#pragma omp parallel for
for (int i = 2; i < 10; i++) {
    factorial[i] = i * factorial[i-1];
}

编译器会把这个循环多线程化,可是并不能实现咱们想要的加速效果,得出的数组含有错误的结构。由于每次迭代都依赖于另外一个不一样的迭代,这被称之为竞态条件。要解决这个问题只可以重写循环或者选择不一样的算法。

竞态条件很难被检测到,由于也有可能刚好程序是按你想要的顺序执行的。

管理公有和私有数据

基本上每一个循环都会读写数据,肯定那个数据时线程之间共有的,那些数据时线程私有的就是程序员的责任了。当数据被设置为公有的时候,全部的线程访问的都是相同的内存地址,当数据被设为私有的时候,每一个线程都有本身的一份拷贝。默认状况下,除了循环变量之外,全部数据都被设定为公有的。能够经过如下两种方法把变量设置为私有的:

  1. 在循环内部声明变量,注意不要是static的
  2. 经过OpenMP指令声明私有变量
// 下面这个例子是错误的
int temp; // 在循环以外声明
#pragma omp parallel for
for (int i = 0; i < 100; i++) {
    temp = array[i];
    array[i] = doSomething(temp);
}

能够经过如下两种方法改正

// 1. 在循环内部声明变量
#pragma omp parallel for
for (int i = 0; i < 100; i++) {
    int temp = array[i];
    array[i] = doSomething(temp);
}
// 2. 经过OpenMP指令说明私有变量
int temp;
#pragma omp parallel for private(temp)
for (int i = 0; i < 100; i++) {
    temp = array[i];
    array[i] = doSomething(temp);
}

Reductions

一种常见的循环就是累加变量,对此,OpenMP 有专门的语句

例以下面的程序:

int sum = 0;
for (int i = 0; i < 100; i++) {
    sum += array[i]; // sum须要私有才能实现并行化,可是又必须是公有的才能产生正确结果
}

上面的这个程序里,sum公有或者私有都不对,为了解决这个问题,OpenMP 提供了reduction语句;

int sum = 0;
#pragma omp parallel for reduction(+:sum)
for (int i = 0; i < 100; i++) {
    sum += array[i];
}

内部实现中,OpenMP 为每一个线程提供了私有的sum变量,当线程退出时,OpenMP 再把每一个线程的部分和加在一块儿获得最终结果。

固然,OpenMP 不止能作累加,凡是累计运算都是能够的,以下表:

操做 私有临时变量初值
+、- 0
* 1
& ~0
| 0
^ 0
&& 1(true)
|| 0(false

循环调度

负载均衡是多线程程序中对性能影响最大的因素了,只有实现了负载均衡才能保证全部的核心都是忙的,而不会出现空闲时间。若是没有负载均衡, 有一些线程会远远早于其余线程结束, 致使处理器空闲浪费优化的可能.

在循环中,常常会因为每次迭代的相差时间较大和破坏负载平衡。一般能够经过检查源码来发现循环的变更可能. 大多数状况下每次迭代可能会发现大概一致的时间,当这个条件不能知足的时候,你可能能找到一个花费了大概一致时间的子集。例如, 有时候全部偶数循环花费了和全部奇数循环同样的时间, 有时候可能前一半循环和后一半循环花费了类似的时间. 另外一方面, 有时候你可能找不到花费相同时间的一组循环. 不论如何, 你应该把这些信息提供给 OpenMP, 这样才能让 OpenMP 有更好的机会去优化循环.

默认状况下,OpenMP认为全部的循环迭代运行的时间都是同样的,这就致使了OpenMP会把不一样的迭代等分到不一样的核心上,而且让他们分布的尽量减少内存访问冲突,这样作是由于循环通常会线性地访问内存, 因此把循环按照前一半后一半的方法分配能够最大程度的减小冲突. 然而对内存访问来讲这多是最好的方法, 可是对于负载均衡可能并非最好的方法, 并且反过来最好的负载均衡可能也会破坏内存访问. 所以必须折衷考虑.

OpenMP 负载均衡使用下面的语法

#pragma omp parallel for schedule(kind [, chunk size])

其中kind能够是下面的这些类型, 而 chunk size 则必须是循环不变的正整数

例子

#pragma omp parallel for
for (int i = 0; i  < numElements; i++) {
    array[i] = initValue;
    initValue++;
}

显然这个循环里就有了竞态条件, 每一个循环都依赖于 initValue 这个变量, 咱们须要去掉它.

#pragma omp parallel for
for (int i = 0; i < numElements; i++) {
    array[i] = initValue + i;
}

这样就能够了, 由于如今咱们没有让 initValue 去被依赖

因此, 对于一个循环来讲, 应该尽量地把 loop-variant 变量创建在 i 上.

待续...

相关文章
相关标签/搜索