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. libmatio开发笔记(一):matlab文件操作libmatio库介绍,编译和基础Demo

    前言   Qt可通过matlab的库对mat文件进行读写,第三方库matio也可以对mat文件进行读写,其已经支持mat文件的7.3版本.   libmatio库介绍   matio软件包含一个用于读 ...

  2. Java JVM——4.程序计数器

    简介 JVM中的程序计数寄存器(Program Counter Register),Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息,CPU只有把数据装载到寄存器才能够运行. 这 ...

  3. go-ini解析ini文件

    文档 https://github.com/go-ini/ini https://ini.unknwon.io/docs/intro/getting_started go get -u gopkg.i ...

  4. 【Azure API 管理】在 Azure API 管理中使用 OAuth 2.0 授权和 Azure AD 保护 Web API 后端,在请求中携带Token访问后报401的错误

    问题描述 在 Azure API 管理中使用 OAuth 2.0 授权和 Azure AD 保护 Web API 后端的文档中操作 "在开发人员门户中启用 OAuth 2.0 用户授权&qu ...

  5. MAUI发布APK初体验

    目的 很早就有想编写安卓程序玩玩的念头了,所以这次学习将MAUI程序生成apk包来玩. 本文apk下载地址:https://azrng.lanzouv.com/iBQRe0eeg8wf ,内容很简单, ...

  6. Java 数组查找

    1 //要找的数 - 数组中的第一个元素 / 最大的数 - 第一个元素 2 //数组的查找(线性查找 二分法查找) 3 //线性查找: 4 //equals 5 6 String dest = &qu ...

  7. 照片也能说话了?嘴型表情全同步,AI数字人时代要来了

    SadTalker是一款先进的人工智能模型,它通过从音频中学习生成3D运动系数,并使用全新的三维面部渲染器来生成头部运动,只需传入一张照片和一段音频,就能生成高质量的AI数字人视频 工作原理 1.显式 ...

  8. liunx 大文件切割,catalina.out 大文件打开

    工作中,由于没有没有配日志文件切割,不小心日志文件上G了,用tail -f   或 cat 命令都难打开了,但偏这时候出了点事,需要查日志 怎么呢.第一条件命令    tail -50000f  ca ...

  9. trans.bat 将.m4a 文件拖拽到这个上面 自动转换成.mp3 老歌精选-歌曲z

    @chcp 65001 >nul echo off :: 获取文件名 SET filePath=%1 :: 因为这里目录的路径是 E:\老歌精选-歌曲z 是11个字符,所以是从第12个字符到最后 ...

  10. In-batch negatives Embedding模型介绍与实践

    语义索引(可通俗理解为向量索引)技术是搜索引擎.推荐系统.广告系统在召回阶段的核心技术之一.语义索引模型的目标是:给定输入文本,模型可以从海量候选召回库中快速.准确地召回一批语义相关文本.语义索引模型 ...