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. 搭建Windows环境下的多功能免费SSH客户端

    关于Windows下的SSH客户端工具,可以有许多选择,从开源免费到商业收费的,零零总总. 免费版: Putty就是最简单的SSH客户端,非常轻量级. Electerm是一个开源可免费使用的跨平台SS ...

  2. python中操作csv

    示例 import csv with open('t.csv', mode='r', encoding='utf-8') as f: reader_obj = csv.reader(f) # 通过re ...

  3. 高性能图计算系统 Plato 在 Nebula Graph 中的实践

    本文首发于 Nebula Graph Community 公众号 1.图计算介绍 1.1 图数据库 vs 图计算 图数据库是面向 OLTP 场景,强调增删改查,并且一个查询往往只涉及到全图中的少量数据 ...

  4. 好用网址分享-77ai导航与77搜索导航

    AI(人工智能)技术正在改变我们的生活方式和工作方式,越来越多的人开始关注和使用AI相关的网站和应用程序.在这篇文章中,我将为大家介绍一些常用的AI网址导航,帮助您更好地了解和使用AI技术. AI H ...

  5. Codeforces Round 922 (Div. 2)(A~D)补题

    A题考虑贪心,要使使用的砖头越多,每块转的k应尽可能小,最小取2,最后可能多出来,多出来的就是最后一块k=3,我们一行内用到的砖头就是\(\frac{m}{2}\)下取整,然后乘以行数就是答案. #i ...

  6. 1.Arduino ESP32配置环境

    ESP32开发板管理器地址 https://dl.espressif.com/dl/package_esp32_index.json // 无效时可以使用下面这个 https://raw.github ...

  7. 【转】客户端软件GUI开发技术漫谈:原生与跨平台解决方案分析

    原生开发应用开发 Microsoft阵营的 Winform WinForm是·Net开发平台中对Windows Form的一种称谓. 如果你想深入的美化UI,需要耗费很大的力气,对于目前主流的CSS样 ...

  8. 浅析三维模型OBJ格式轻量化压缩文件大小的技术方法

    浅析三维模型OBJ格式轻量化压缩文件大小的技术方法 在减小三维模型OBJ格式轻量化文件大小方面,有许多技术和方法可以使用.下面我将介绍一些常用的方法来减小OBJ文件的大小. 1.优化顶点数量:减少OB ...

  9. 记录--妙用computed拦截v-model,面试管都夸我细

    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 如何避免写出屎山,优雅的封装组件,在面试官面前大大加分,从这篇文章开始! 保持单向数据流 大家都知道vue是单项数据流的,子组件不能直接修 ...

  10. OpenLayers绘制热力图 代码记录

    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 做地图开发,往往需要掌握专题地图制作的技能.今天用OpenLayers6来做一个热力图的效果. 页面效果: 代码部分: <!DOCT ...