OpenMP优化for循环的基础运用

OpenMP作为多线程并行优化API,其使用方式与C++自带的多线程使用方式有很大的不同。

在使用OpenMP时,我们是通过 #pragma omp+字句 所组成的命令对线程的行为进行控制,之后编译器会自动对这些命令进行分析与优化,将相关代码由串行变为并行。

整个过程中编译器已经替我们做了相当多的工作,大多数情况下只需要略微的改动就能将程序由串行转化为并行,从而达到成倍的性能提升。

预先准备

因为是让编译器进行优化,所以需要加上-fopenmp的编译选项来开启这一功能。

在未启用openmp的情况下编译器会直接忽视#pragma omp相关命令,将其当成普通程序编译,并不会发出提醒或报错。

如果要判断openmp是否成功启用的话,可以对宏 _OPENMP 定义进行检测

#ifdef _OPENMP
puts("OpenMp available");
#else
puts("OpenMp unavailable");
#endif

1.parallel简单并行

这里以Hello World为例:

#include <stdio.h>

int main()
{
printf("Hello World!\n");
}

很明显,执行后的输出是

Hello World!

然后加上OpenMP命令

#include <stdio.h>

int main()
{
#pragma omp parallel
printf("Hello World!\n");
}

输出结果变成了

Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!

可以看到Hello World被输出了8次。

这是因为#pragma omp parallel之后的代码被转化为了多线程执行,而输出8次就代表他使用了8线程。


2.num_threads设置线程数

默认的线程数量和CPU有关,不过我们也可以自行指定。

一种方法是设置 OMP_NUM_THREADS 环境变量,

另一种则是使用 num_threads(n) 进行调整,括号里的n就代表线程数。

#include <stdio.h>

int main()
{
int threads_num = 2; // 甚至可以用变量 #pragma omp parallel num_threads(threads_num)
printf("Hello World!\n");
}

现在就是调用2个线程运行了。

Hello World!
Hello World!

3.for循环

大多数情况下的并行需求都出现在for循环中,而OpenMP自然也有对for循环进行优化。

这里我们导入 <omp.h> 头文件,里面包含了OpenMP提供的函数方便调试和控制。

其中的 omp_get_thread_num() 可以返回执行这段代码的线程id。

用一个简单循环打印程序进行演示:

#include <stdio.h>
#include <omp.h> int main()
{
#pragma omp parallel for
for (int i = 0; i < 10; i++)
printf("i=%d thread:%d\n", i, omp_get_thread_num());
}

输出如下:

i=0 thread:0
i=1 thread:0
i=7 thread:5
i=5 thread:3
i=6 thread:4
i=8 thread:6
i=4 thread:2
i=9 thread:7
i=2 thread:1
i=3 thread:1

可以看出循环变为了乱序执行,这也反映出了循环并行的效果。

但同时也表明,进行多线程处理的for循环一定是独立并且前后不相干,否则这种乱序执行的特性就会导致BUG出现。

for (int i = 1; i < 10; i++)
{
a[i] = a[i - 1] + 1;
}

像是上面这种进行数据修改并且前后依赖的循环就会因为乱序执行而出BUG。


4.三种private

既然有多个线程在同时运行,那么他们对变量是如何进行管理的呢?

默认条件下有两种情况:

  1. 创建线程之前就存在的变量——所有线程共享

  2. 线程运行时创建的局部变量——每个线程独有

既然这只是默认情况,那就代表我们其实可以用命令进行修改。

先写一个普通的二层嵌套循环,这里的i和j就是在多线程循环开始之后才进行创建的,属于情况2。

因此每个线程都有属于自己的i和j的拷贝,修改和访问时不会相互干扰,从而保证了循环的正常进行。

#include <stdio.h>

int main()
{
#pragma omp parallel for
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; j++)
printf("i=%d j=%d\n", i, j);
}

运行一下

i=2 j=0
i=2 j=1
i=2 j=2
i=0 j=0
i=0 j=1
i=0 j=2
i=1 j=0
i=1 j=1
i=1 j=2

输出的结果也符合我们的预期。

现在我们把i、j的创建放到开启多线程之前。

就变成了情况1,此时i和j就变成了 所有线程共享 的变量。

#include <stdio.h>

int main()
{
int i, j; #pragma omp parallel for
for (i = 0; i < 3; i++)
for (j = 0; j < 3; j++)
printf("i=%d j=%d\n", i, j);
}

这时如果我们再次运行

i=1 j=0
i=1 j=1
i=1 j=2
i=0 j=0
i=2 j=0

这时期待已久的BUG就出现了。

这就是多个线程同时访问和修改相同变量导致的问题,

private

为此OpenMP也非常贴心地提供了 private() 命令来解决。

只需要加上 private() 并在括号里写上变量名,

就能让 每个线程都有该变量的独立拷贝,相当于一个同名的局部变量

#include <stdio.h>

int main()
{
int i, j; #pragma omp parallel for private(i, j)
for (i = 0; i < 3; i++)
for (j = 0; j < 3; j++)
printf("i=%d j=%d\n", i, j);
}

虽然private声明变量后,每个线程都会生成一个相应的拷贝。

但这些线程并不会对他们进行初始化。

#include <stdio.h>

int main()
{
int x = -1; #pragma omp parallel for private(x)
for (int i = 0; i < 5; i++)
printf("x=%d\n", x);
}

可以看到拷贝出的变量是什么值都有,但就是没有和原来的值相同的。

x=14908664
x=13903992
x=13909720
x=13905304
x=8

firstprivate

想要有初始化的话就需要用到 firstprivate()

#include <stdio.h>

int main()
{
int x = -1; #pragma omp parallel for firstprivate(x)
for (int i = 0; i < 5; i++)
printf("x=%d\n", x);
}

这样每个变量就都能初始化了

x=-1
x=-1
x=-1
x=-1
x=-1

虽然现在我们有了初始化,但循环结束后变量的值还是无法保留。

继续举个例子测试一下:

(private和firstprivate同理)

#include <stdio.h>

int main()
{
int x = -1;
printf("start x=%d\n", x); #pragma omp parallel for private(x)
for (int i = 0; i < 5; i++)
printf("x=%d\n", x = i); printf("final x=%d", x);
}

输出结果

start x=-1
x=1
x=2
x=3
x=0
x=4
final x=-1

可以看到使用private时,循环结束后x的值是不会保留的。

lastprivate

这时候 lastprivate() 就派上用场了,它的功能就是在private的基础上,能够在循环结束时保留变量的值。

改成lastprivate。

#include <stdio.h>

int main()
{
int x = -1;
printf("start x=%d\n", x); #pragma omp parallel for lastprivate(x)
for (int i = 0; i < 5; i++)
printf("x=%d\n", x = i); printf("final x=%d", x);
}

输出结果

start x=-1
x=3
x=2
x=1
x=4
x=0
final x=4

变量是成功保存了,但为什么明明最后一个输出的是3,保存的结果却是4呢?

这是因为lastprivate保存的变量是 逻辑上的最后的值

从代码运行逻辑上来讲x最后的值是4,所以结果就是4。

说通俗点就是和单线程运行的结果相同,不受乱序执行的影响。

组合使用

从功能上可以看出,firstprivate是对功能private的扩展,二者是相互替代的关系,所以 不能同时使用(会编译失败)。

lastprivate和private同样也是相互替代的关系,依旧 不能同时使用(会编译失败)。

但是firstprivate和lastprivate在功能上有相互补充关系,所以 可以同时使用


5.reduction

假如现在我们要做一个高斯最喜欢的数学题,进行1~100的求和。

使用循环累加并且加上OpenMP并行优化。

#include <stdio.h>

int main()
{
int sum = 0; #pragma omp parallel for
for (int i = 1; i <= 100; i++)
sum += i; printf("sum=%d", sum);
}

稍微尝试几次之后就会发现,有时候输出的答案并不是5050。

到现在你应该可以很容易就能想到,这是多个线程同时访问sum时产生冲突导致的。

但在这种情况下用private就没办法对其进行求和,此时 reduction 就派上用场了。

reduction的命令格式是 reduction(operation : variable),其中operation是操作类型,variable则是操作变量。

reduction的作用就是给每个线程创建一个独立的变量,在结束后根据操作类型进行归约。

默认操作包括

  • 算数运算:+, *, -, max, min

  • 逻辑运算:&&, ||

  • 位运算:&, |, ^

我们需要对sum进行累加,所以应该使用 reduction(+ : sum)

#include <stdio.h>

int main()
{
int sum = 0; #pragma omp parallel for reduction(+ : sum)
for (int i = 1; i <= 100; i++)
sum += i; printf("sum=%d", sum);
}

这样就能正常得到结果了。


6.对于首个for循环迭代器的限制

还有一个比较细节的地方就是OpenMP在优化for语句时,会自动把第一个循环的迭代器(也就是i)设为private

比如在前面演示private时举例的双重for循环中,如果我们将

#pragma omp parallel for private(i, j)

修改为

#pragma omp parallel for private(j) // 不显式声明i为private

修改后并不会影响循环的正常运行。

而从下面这个例子则能够更好地证实这一点。

在代码中,我们将循环外ij的初值设为-1,并在循环内不断改变i和j的值,观察循环结束后二者的变化情况。

#include <stdio.h>

int main()
{
int i = -1, j = -1; #pragma omp parallel for
for (i = 0; i < 5; i++)
{
printf("i=j=%d\n", j = i);
} printf("final i=%d j=%d\n", i, j);
}

运行结果为:

i=j=0
i=j=2
i=j=1
i=j=4
i=j=3
final i=-1 j=3

可以看出循环体内外的两个i的值是不同的,循环体里的i更像是for循环的局部变量。

这是OpenMP为了保证循环能够正常运作而进行的优化,

但这种默认且强制性的优化的代价是语法规则的拘束,一些正常可以通过编译的代码,放在parallel for的首个循环就会出现编译错误的情况。

#include <stdio.h>

int main()
{
int i, j; #pragma omp parallel for // 去掉for优化后才能编译
for (i = 0, j = 0; i < 5; i++) // 只是加了个j=0就报错了
{
}
}

参考文献

OpenMP的简单使用教程

OpenMP 入门与实例分析

OpenMP--private, shared变量

OpenMP用法大全(个人整理版)

C++多线程编程#pragma omp parallel

OpenMP并行开发(C++)

OpenMP Reductions


本文发布于2022年12月5日

最后修改于2022年12月5日

OpenMP优化for循环的基础运用的更多相关文章

  1. 【ABAP系列】SAP ABAP 优化LOOP循环的一点点建议

    公众号:SAP Technical 本文作者:matinal 原文出处:http://www.cnblogs.com/SAPmatinal/ 原文链接:[ABAP系列]SAP ABAP 优化LOOP循 ...

  2. while循环语句基础

    while循环语句基础 一while循环语句介绍 循环语句命令常用于重复执行一条指令或一组指令,直到条件不再满足时停止,   Shell脚本语言的循环语句常见的有while, until, for及s ...

  3. for、while循环(java基础知识四)

    1.循环结构概述和for语句的格式及其使用 * 什么是循环结构 循环语句可以在满足循环条件的情况下,反复执行某一段代码,这段被重复执行的代码被称为循环体语句,当反复执行这个循环体时,需要在合适的时候把 ...

  4. 关于优化for循环的注意的事项

    for循环注意事项: 1.for循环内部尽量少做数据库查询之类的IO代价大的操作 2.尽量控制for循环的次数,不多做无用功 3.能一次加载在内存中的,就不要通过循环来多次查询数据库,除非数据量过大. ...

  5. 【openmp】for循环的break问题

    问题描述:在用openmp并行化处理for循环的时候,便无法在for循环中用break语句,那么我们如何实现这样的机制呢?在stackoverflow上看到一个不错的回答总结一下. volatile ...

  6. [ DLPytorch ] 文本预处理&语言模型&循环神经网络基础

    文本预处理 实现步骤(处理语言模型数据集距离) 文本预处理的实现步骤 读入文本:读入zip / txt 等数据集 with zipfile.ZipFile('./jaychou_lyrics.txt. ...

  7. PHP循环语句基础介绍

    PHP 中的循环语句用于执行相同的代码块指定的次数. 循环 在您编写代码时,您经常需要让相同的代码块运行很多次.您可以在代码中使用循环语句来完成这个任务. 在 PHP 中,我们可以使用下列循环语句: ...

  8. for循环的基础使用

    for循环:    for 变量名 in 列表:do           循环体    done    执行机制:           依次将列表中的元素赋值给“变量名”:每次赋值后即执行一次循环体: ...

  9. mysql 数据库优化第一篇(基础)

    Mysql数据库优化 1. 优化概述 存储层:存储引擎.字段类型选择.范式设计 设计层:索引.缓存.分区(分表) 架构层:多个mysql服务器设置,读写分离(主从模式) sql语句层:多个sql语句都 ...

  10. 4.关于while循环的基础小练习

    1)使用while.if循环输入123456 8910 count = 0 while count < 10: count += 1 if count == 7: print('') else: ...

随机推荐

  1. win32 - 使用Desktop Duplication API复制桌面图像

    该代码来源于codeproject,经过测试发现,在屏幕处于旋转的情况下捕获的图像是黑色的.暂时没有找到原因. 代码开箱即用, #define WIN32_LEAN_AND_MEAN #include ...

  2. pikachu SQL-inject insert/update注入

    insert 注入 (修改信息处是update注入,和此处同理) 注册页面,用户处输入 1' 发现报错信息 You have an error in your SQL syntax; check th ...

  3. context讲解

    context包 context包介绍 ​ 在go语言中,每个独立调用一般都会被单独的协程处理.但在处理一个请求时,往往可能需要在多个协程之间进行信息传递,甚至包括一层层地递进顺序传递,而且这种信息往 ...

  4. Centos8上安装Redis5.X

    一.下载Redis 下载地址:wget http://download.redis.io/releases/redis-5.0.7.tar.gz 解压:tar -xzvf redis-5.0.7.ta ...

  5. SpringBoot Starter大全

    spring Boot应用启动器基本的一共有44种,具体如下 1)spring-boot-starter 这是Spring Boot的核心启动器,包含了自动配置.日志和YAML. 2)spring-b ...

  6. Java 设计模式简介

    设计模式简介 设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用.设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案.这些解决方案是众多软 ...

  7. Linux系统查看主机性能

    查看主机的CPU性能: cat  /proc/cpuinfo cat /proc/meminfo |grep MemTotal    内存信息 查看物理cpu个数:cat /proc/cpuinfo ...

  8. Scriban语言手册中文版

    Scriban是一个快速.强大.安全且轻量级的模板引擎,同时兼容liquid语法规则. 项目地址:https://github.com/scriban/scriban 这个文档是语言语法的中文翻译 原 ...

  9. puppeteer 提交 gitee - win10 (放弃,改成手点)async.series

    puppeteer 提交 gitee 需求 不想每次都登录到gitee上点击发布,想自动点击. 用puppeteer 模拟下 现在是win10环境,安装比较费尽 npm i puppeteer 这里用 ...

  10. inputNextFocus vue - js 跳转 下一个 tab

    inputNextFocus vue - js 跳转 下一个 tab <template> <Input v-model="val1" ref="inp ...