多核编程学习笔记之OpenMP(一)

2023-11-16

多核编程学习笔记之OpenMP(一)

I.  配置及简介

1.1 在VC++2008(VC9.0)中,如果没有任何设置,在代码中使用编译指导语句将不会报错,但是也不起作用。

 

1.2 OpenMP发展与优势

1.2.1 OpemMP的规范由SGI发起,它是一种面向共享内存以及分布式共享内存的多处理器多线程并行编程语言。OpenMP是一种共享内存并行的应用程序编程接口。所有的处理器都被连接到一个共享的内存单元上,处理器在访问内存的时候使用的是相同的内存编址空间。由于内存是共享的,因此,某一处理器写入的数据会立刻被其他处理器访问到。

    OpenMP具有良好的可移植性,支持Fortran和C/C++编程语言,操作系统平台方面则支持UNIX系统以及Windows系统。OpenMP的重要性在于,它能够为编写多线程程序提供一种简单的方法,而无需程序员进行复杂的线程创建、同步、负载平衡和销毁工作。

1.2.2 OpemMP的变成模型以线程为基础,通过编译指导语句来显示地并行化,为编程人员提供了对并行化的完整的控制。在并行执行的时候,主线程和派生线程共同工作。在并行代码结束执行后,派生线程退出或者挂起,不再工作,控制流回到单独的主线程中。

OpenMP的功能由两种形式提供,编译指导语句和运行时库函数。

1)编译指导语句

编译指导语句的含义是在编译器编译程序的时候,会识别特定的注释,而这些特定的注释就包含这OpenMP的程序的一些语义。例如在C/C++程序中,用#pragma omp parallel来标志一段并行程序块。在一个无法识别OpenMP语义的编译器中,会将这些特定的注释当做普通的程序注释而被忽略。因此,仅使用编译指导语句编写的OpenMP程序就能够同时被普通编译器和支持OpenMp的编译器处理。这种性质带来的好处就是用户可以使用同一份代码来编写串行和并行的程序,或者在把串行程序改编成并行程序的时候,保持串行源代码部分不变,从而极大地方便了程序编写人员。

编译指导语句的形式为:

#pragam omp <directive> [clause[[,] clause]. . .]

其中directive部分就包含了具体的编译指导语句,包括parallel, for, parallel for, section, sections, single, master, critical, flush, ordered和atomic。这些编译指导语句或者用来分配任务,或者用来同步。后面可选的子句clause给出了相应的编译指导语句的参数,子句可以影响到编译指导语句的具体行为,每一个编译指导语句都有一系列适合它的子句。其中parallel、for、sections、section等主要用来创建线程。

2)运行时库函数

另外一种提供OpenMP功能的形式就是OpenMP的运行时库函数,它用于设置和获取执行环境的相关信息,它们当中也包含一系列用以同步的API。要使用运行时库函数所包含的库函数,必须在相应的源文件中包含头文件omp.h。OpenMP库函数类似于相应编程语言内部的函数调用,因此在没有库支持的编译器上就无法正确识别OpenMP程序,这是库函数与编译指导语句不同的地方。

编译指导语句的优势体现在编译阶段,对于运行阶段则支持较少。OpenMP提供了运行时库函数来支持运行时对并行环境的改编和优化,但这种方式打破了源代码在串行和并行之间的一致性。

1.2.3 IDE

Microsoft Visual Studio2005与2008都完全支持OpenMP编程。安装之后,无需另外其他的软件,通过新的编译器选项/openmp来支持OpenMP程序的变异和连接,编译器会自动地将用户的代码和OpenMP在windows下实现库vcomp.dll链接在一起。程序在运行的时候会自动地寻找vcomp.dll。

1.3 OpenMP多线程应用程序性能分析

影响性能的主要因素有:

1)OpenMP本身的开销

    OpenMP获得应用程序多线程并行化的能力不是凭空而来的,它需要程序库的支持,库中代码的运行必然会带来一定的开销。实际上并不是所有的代码都需要并行化,有些代码在并行化后的执行效率反而不如串行时高,原因就是加上了并行化所带来的开销后,代码的执行效率降低。因此,只有在并行执行代码负担足够大,而引入OpenMP本身的开销又足够小时,引入并行化操作才能提高程序执行效率。

2)负载均衡

    已知一个OpenMP应用程序在执行的过程中,有很多的同步点,线程只有在进行同步之后才能继续执行下面的代码。因此某一个线程在执行到同步点之后,若没有进一步的工作需要完成,此线程只有等待其它线程执行完毕后才能继续执行。此时,如果各个线程之间的负载不均衡,就有可能出现某些线程“空等”,而另外一些线程因负担沉重,要很长事件才能完成任务。

3) 线程同步带来的开销

    线程之间存在同步开销是多线程应用程序的特点,在进行同步时候必然会带来一定的开销。很多情况下,不合适的同步机制或算法会使代码的运行效率下降。

 

1.4 多核编程的几个难题及其应对策略

1)加速系数:衡量多处理器系统的性能是,通常要用到的一个指标叫做加速系数,定义如下:

S(p) = 使用单处理器执行时间(最好的顺序算法) / 使用具有p各处理器所需执行的时间

2)阿姆达尔定律(Amdahl)
S(p) = p / (1 + (p – 1) * f)

其中S(p)表示加速系数,p表示处理器个数,f表示串行部分所占整个程序执行时间的比例。

当f = 5%, p = 20时, S(p) = 10.256左右
当f = 5%, p = 100时, S(p) = 16.8左右

也就是说只要串行部分比例占5%时,当处理器个数从20增加到100个时,加速系数只能从10.256增加到16.8左右,处理器个数增加了5倍,速度只增加了60%多一点。即使处理器个数增加到无穷多个,加速系数的极限值也只有20。

按照Amdahl定律的话,可以说多核方面几乎没有任何发展前景,即使软件中只有1%的不可并行部分,那么最大加速系系统也只能达到100,再多CPU也无法提升速度性能。按照这个定律,可以说多核CPU的发展让摩尔定律延续不了多少就会达到极限。

3)Gustafson定律

Gustafson认为软件中的串行部分是固定的,不会随规模的增大而增大,并假设并行处理部分的执行时间固定(服务器软件可能就是这样)。Gustafson定律描述如下:

S(p) = p + (1 – p) * fts

如果串行比例为5%,处理器个数为20个,那么加速系数为20+(1-20)*5%=19.05
如果串行比例为5%,处理器个数为100个,那么加速系数为100+(1-100)*5%=95.05

Gustafson定律中的加速系数几乎跟处理器个数成正比,如果现实情况符合Gustafson定律的假设前提的话,那么软件的性能将可以随处理器个数的增加而增加。

4)实际情况中的串行化分析
阿姆尔达定律和Gustafson定律的计算结果差距如此之大,那么现实情况到底是符合那一个定律呢?我个人认为现实情况中既不会象阿姆尔达定律那么悲观,但也不会象Gustafson定律那么乐观。为什么这样说呢?还是进行一下简单的分析吧。
首先需要确定软件中到底有那么内容不能并行化,才能估计出串行部分所占的比例,20世纪60年代时,Bernstein就给出了不能进行并行计算的三个条件:
条件1:C1写某一存储单元后,C2读该单元的数据。称为“写后读”竞争
条件2:C1读某一存储单元数据后,C2写该单元。称为“读后写”竞争
条件1:C1写某一存储单元后,C2写该单元。称为“写后写”竞争
满足以上三个条件中的任何一个都不能进行并行执行。不幸的是在实际的软件中大量存在满足上述情况的现象,也就是我们常说的共享数据要加锁保护的问题。
加锁保护导致的串行化问题如果在任务数量固定的前提下,串行化所占的比例是随软件规模的增大而减小的,但不幸的是它会随任务数量的增加而增加,也就是说处理器个数越多,锁竞争导致的串行化将越严重,从而使得串行化所占的比例随处理器个数的增加而急剧增加。(关于锁竞争导致的串行化加剧情况我会在另一篇文章中讲解)。所以串行化问题是多核编程面临的一大难题。

5)可能的解决措施
对于串行化方面的难题,首先想到的解决措施就是少用锁,甚至采用无锁编程,不过这对普通程序员来说几乎是难以完成的工作,因为无锁编程方面的算法太过于复杂,而且使用不当很容易出错,许多已经发表到专业期刊上的无锁算法后来又被证明是错的,可以想象得到这里面的难度有多大。
第二个解决方案就是使用原子操作来替代锁,使用原子操作本质上并没有解决串行化问题,只不过是让串行化的速度大大提升,从而使得串行化所占执行时间比例大大下降。不过目前芯片厂商提供的原子操作很有限,只能在少数地方起作用,芯片厂商在这方面可能还需要继续努力,提供更多功能稍微强大一些的原子操作来避免更多的地方的锁的使用。

第三个解决方案是从设计和算法层面来缩小串行化所占的比例。也许需要发现实用的并行方面的设计模式来缩减锁的使用,目前业界在这方面已经积累了一定的经验,如任务分解模式,数据分解模式,数据共享模式,相信随着多核CPU的大规模使用将来会有更多的新的有效的并行设计模式和算法冒出来。
第四个解决方案是从芯片设计方面来考虑的,由于我对芯片设计方面一无所知,所以这个解决方案也许只是我的一厢情愿的猜想。主要的想法是在芯片层面设计一些新的指令,这些指令不象以前单核CPU指令那样是由单个CPU完成的,而是由多个CPU进行并行处理完成的一些并行指令,这样程序员调用这些并行处理指令编程就象编写串行化程序一样,但又充分利用上了多个CPU的优势。

 

1.5 多核编程好文摘录

多核处理器的9大关键技术
关于多核的一些概念和区别
关于多核编程的一些想法
多核体系结构的发展
多核软件设计方案
多核研究现状

 

1.6

 

II. 语法

2.1 使用#pragma omp parallel语句的时候注意花括号写法:

CODE:

#pragma omp parallel num_threads(2) {
            cout << "Second level, ThreadID = " << omp_get_thread_num() << endl;
        }

上面的写法是错误的,将提示错误的pragma指令。

CODE:

#pragma omp parallel num_threads(2)
        {
            cout << "Second level, ThreadID = " << omp_get_thread_num() << endl;
        }

上面的代码才是正确的。

 

2.2 OpenMP中的编译指导语句各市。

“编译指导语句后面需要跟一个new-line(换行符),然后跟着的是一个structured-block,比如一个for循环或者一个花括号对(及内部的全部代码)都是一个structured-block”

“当编译指导语句较长(如80字符),可以使用多行书写,用C/C++的续行符’/’连接起来即可”

“parallel命令是用来构造一个并行快的,也可以使用其他命令,如for、sections等与之配合使用。在C/C++中,parallel命令的使用方法如下:

#paragma omp parallel [for | sections] [子句[子句]]

{

    // 代码

}

——摘自《多核计算与程序设计》

但是使用一个OpenMP函数的时候,可以只引入<omp.h>即可。比如cout << "Num of Cores: " << omp_get_num_procs() << endl;就不需要编译指导语句直接输出。

 

2.3 OpenMP中,如果不指定创建的线程个数,则默认创建的线程数与CPU核心数一致。在执行for循环的时候,分配的线程也是与CPU核心数一致。假如有两个核心,for共循环4次,则最终只有两个线程去执行。

CODE:

#pragma omp parallel for
    for (int i = 0; i < 4; i++) {
        cout << i << ", " << omp_get_thread_num() << endl;
    }

输出:

2, 1
3, 1
0, 0
1, 0

 

#pragma omp parallel for num_threads(4)
    for (int i = 0; i < 4; i++) {
        cout << i << ", " << omp_get_thread_num() << endl;
    }

输出:

1, 1
2, 2
3, 3
0, 0

 

2.4 section与sections

sections和section命令是OpenMP里面用来创建线程的另外一种方式,先用sections定义一个区块,然后用section将sections区块划分成几个不同的段,每段都并行执行。与for、parallel一样,如果不使用num_thread指定要创建的线程数,则默认创建的线程数与CPU核数相同。

CODE:

#pragma omp parallel sections
    {
#pragma omp section
        {
            cout << "Section1, id= " << omp_get_thread_num() << endl;
        }
#pragma omp section
        {
            cout << "Section2, id= " << omp_get_thread_num() << endl;
        }
#pragma omp section
        {
            cout << "Section3. id= " << omp_get_thread_num() << endl;
        }
#pragma omp section
        {
            cout << "Section4. id= " << omp_get_thread_num() << endl;
        }
    }

结果:

Section1, id= 1
Section3. id= 1
Section4. id= 1
Section2, id= 0

可见只创建了两个线程。

注意:使用section语句时,应该注意的是这种方式需要保证各section里的代码执行时间相差不大;如果某个section执行时间比其他section的过场,就达不到并并行执行的效果。

 

2.5 for的四种写法区别

第一种:

CODE:

#pragma omp parallel
    {
        for (int i = 0; i < 4; i++) {
            cout << i << " , id = " << omp_get_thread_num() << endl;
        }
        cout << "Finish!" << endl;
    }

输出:

0 , id = 0
1 , id = 0
2 , id = 0
3 , id = 0
Finish!
0 , id = 1
1 , id = 1
2 , id = 1
3 , id = 1
Finish!

 

第二种:

CODE:

#pragma omp parallel for
    for (int i = 0; i < 4; i++) {
        cout << i << " , id = " << omp_get_thread_num() << endl;
    }
    cout << "Finish!" << endl;

输出:

2 , id = 1
3 , id = 1
0 , id = 0
1 , id = 0
Finish!

 

第三种:

CODE:

#pragma omp parallel
    {
#pragma omp for
        for (int i = 0; i < 4; i++) {
            cout << i << " , id = " << omp_get_thread_num() << endl;
        }
        cout << "Finish!" << endl;
    }

输出:

2 , id = 1
3 , id = 1
0 , id = 0
1 , id = 0
Finish!
Finish!

 

第四种:

CODE:

#pragma omp for
      for (int i = 0; i < 4; i++) {
          cout << i << " , id = " << omp_get_thread_num() << endl;
      }
      cout << "Finish!" << endl;

输出:

0 , id = 0
1 , id = 0
2 , id = 0
3 , id = 0
Finish!

 

总结如下:

使用第一种方法,虽然使用了双线程,但确实串行地执行了两次。

第二种方法是并行,应该提倡的方法。但要注意,parallel for之后不能跟大括号,否则提示错误:error C3014: OpenMP“parallel for”指令后应为 for 循环

第三种方法也是并行,与第二种的区别是,第二种方法并行部分仅仅是for循环,之后的输出则属于串行部分,因此只输出一次。而第三种方法的输出在parallel作用域内,因此属于并行部分,被执行了两次。

第四种单独使用for,仅仅使用单线程。

 

注意:for结构体必须紧跟在#pragma omp for之后,中间不能有花括号!

 

2.6 sections与section

1)section必须被嵌套在sections中使用,否则报以下异常:error C3044: “section”: 只允许直接嵌套在 OpenMP“sections”指令中

CODE:

#pragma omp section
    {
        cout << "Using only section." << endl;
    } // ERROR

2)在sections中只能执行一条语句,如果想要执行其他语句,则必须使用#pragma omp section。此外注意sections必须跟花括号,即使只有一条语句被执行。

CODE:

#pragma omp sections
    {
        cout << "Using only section." << endl;
        cout << "Wrong!" << endl;
    }

第二条cout必须加#pragma omp section

3)仅仅使用sections不能创建多个线程,必须以下代码:

CODE:

#pragma omp sections
    {
#pragma omp section
        cout << "Using only section. id=" << omp_get_thread_num() << endl;
#pragma omp section
        {
            for (int i = 0; i < 5; i++) {
                cout << i << ", id=" << omp_get_thread_num() << endl;
            }
        }
    }

输出:

Using only section. id=0
0, id=0
1, id=0
2, id=0
3, id=0
4, id=0
Finish!

 

必须将parallel与sections配合使用,如:

CODE:

#pragma omp parallel sections
    {
#pragma omp section
        cout << "Using only section. id=" << omp_get_thread_num() << endl;
#pragma omp section
        {
            for (int i = 0; i < 5; i++) {
                cout << i << ", id=" << omp_get_thread_num() << endl;
            }
        }
    }

 

X. 异常

x.1 error C3861: “omp_get_thread_num”: 找不到标识符

需要包含omp.h

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

多核编程学习笔记之OpenMP(一) 的相关文章

  • 【vulnhub靶机】DC-3

    原知识星球老文搬运 拿到靶机之后导入到virtualBOX里面 1 nmap扫描主机存活 192 168 56 104 有个80端口 不放心的话可以用masscan 2 直接访问看下 这里提示只有一个flag 直接拿到root权限 3 习惯

随机推荐

  • uniapp开发的h5网页如何去掉网址里的#号

    在manifest json里配置history模式 这里特别注意下面的 运行的基础路径 里不要写 因为这个默认会强制hash模式 如图 然后再服务器端配置下规则 history模式下配置nginx location try files u
  • GPL和MIT开源协议

    GPL GNU通用公共许可证简称为GPL 是由发行的用于计算机软件的协议证书 使用该证书的软件被称为自由软件 大多数的GNU程序和超过半数的自由软件使用它 GPL的出发点是代码的开源 免费使用和引用 修改 衍生代码的开源 免费使用 但不允许
  • char码值对应列表大全

    Char 0 为0的字符 Char 1 Char 2 Char 3 Char 4 Char 5 Char 6 Char 7 响铃 Char 8 回格 Char 9 tab 水平制表符 Char 10 换行 Char 11 tab 垂直制表符
  • Dump文件的生成以及使用WinDbg静态分析

    前言 本文章主要介绍了如何生成Dump文件 包括两种方式 通过代码生成和通过注册表生成 并且介绍了WinDbg工具的下载和使用 以及如何使用WinDbg工具去静态分析Dump文件 从而找到程序的崩溃位置 生成Dump文件 通过调用WinAP
  • cas 编译安装依赖时提示: Failure to find net.shibboleth.tool:xmlsectool:jar:2.0.0

    错误信息 Could not resolve dependencies for project org apereo cas cas overlay war 1 0 Failure to find net shibboleth tool x
  • 本地 Django 部署 Heroku的时候某个 / 某些数据库显示总是无法创建成功 relation “nnsh_backend_new_userinfo“ does not exist LINE

    文章目录 情景 原因 操作 手动 自动 情景 假设你有一个项目 A 你之前部署了项目 A 里面包含了两个数据库的表 table1 和 table2 他们都顺利部署 然后你相加一些功能 于是又创建了一张表 table3 于是再部署的时候发现
  • glBindFragDataLocation

    异构计算GLSL学习笔记 1 原文地址 http blog csdn net hjimce article details 51475644 作者 hjimce 最近开始学习深度学习的一些gpu编程 大体学了cuda后 感觉要在手机上跑深度
  • python-查看帮助

    help 一 不同的环境下 1 交互模式下 命令行 查看模块的帮助信息 import pickle help pickle 可以看到详细信息 More 上回车 滚动信息 q 退出帮助 2 ide里 需要做一个输出 import pickle
  • unity基础编程(一)

    以此来记录系统学习使用unity的知识方便以后复习使用 如果能得到监督和指导 不胜感激 unity常用使用快捷键 1 Q 抓手工具 W 移动工具 E 旋转工具 R 缩放工具 T 横切面工具 就在键盘一排试一试就会很清楚了 2 Z 轴点模式切
  • 自动在图片上添加页码

    在一次工作中 需要对几百GB的图片文件添加页码 也就是在图片添加一定的流水号 那么 在图片上添加页码 总的需要四个步骤 1 图片重命名 批量修改原图片名 设置流水号作为图片文件名 如 0001 gt 0036 2 添加页码 通iSee软件批
  • Docker赋能物联网:探索软件供应链的优势、挑战和安全性

    作者 JFrog大中华区总经理董任远 随着联网设备硬件性能的日益提升及价格愈发低廉 物联网应用的复杂性随之提升 常用的容器化平台Docker能够帮助精简流程 助力开发人员更轻松地创建和维护物联网应用 本文将探讨Docker为物联网开发带来的
  • 最大熵原理

    最近看到一位高手 说了最大熵原理应用在排名 让我倍感发抖 网上有个人连研究基本步骤都写完了 着实让蛋疼了一小下 就引用一下吧 最大熵原理在1957 年由E T Jaynes 提出的 主要思想是 在只掌握关于未知分布的部分知识时 应该选取符合
  • 第1.2章 神经网络中隐藏层、偏置单元、激活函数的作用(使用激活函数的原因)

    神经网络中隐藏层 偏置单元 激活函数的作用 隐藏层 偏置单元 激活函数 权重 摘要 这篇文章主要是对上一篇文章中所给例题中部分知识点的讲解 较多的参考了网上其他朋友的资料 主要是供大家学习和自己以后查看资料方便 如有侵权 请告知 我会及时修
  • git 回滚

    1 没有push 这种情况发生在你的本地代码仓库 可能你add commit 以后发现代码有点问题 准备取消提交 用到下面命令 reset git reset soft mixed hard 上面常见三种类型 mixed 会保留源码 只是将
  • C语言函数大全-- r 开头的函数

    r 开头的函数 1 raise 1 1 函数说明 1 2 演示示例 1 3 运行结果 2 rand 2 1 函数说明 2 2 演示示例 2 3 运行结果 3 read 3 1 函数说明 3 2 演示示例 3 3 运行结果 4 realloc
  • DFS 刷题记录(laptop part)(2022.2.10)

    namespace matchstick my int isDividedby4 vector
  • 树的遍历(bfs)

    题目链接 https www acwing com problem content 1499 题目 一个二叉树 树中每个节点的权值互不相同 现在给出它的后序遍历和中序遍历 请你输出它的层序遍历 输入格式 第一行包含整数 N N N 表示二叉
  • 初识操作系统

    初识操作系统 1 硬件组成 冯诺依曼体系结构 2 操作系统 Operator System 概念 OS定位 设计OS的目的 3 进程 进程的初步认识 时间片 并发与并行 内核态与用户态 进程中的上下文 进程状态 进程 线程小结 进程小结 线
  • Gitee使用

    文章目录 前言 一 Gitee简介 二 使用Gitee 1 创建仓库 2 上传代码 3 团队协作 4 Issue管理 5 CI CD集成 6 社交化 结语 前言 开源软件开发是当今互联网时代的一项重要活动 在开源社区 有许多平台可以帮助开发
  • 多核编程学习笔记之OpenMP(一)

    多核编程学习笔记之OpenMP 一 I 配置及简介 1 1 在VC 2008 VC9 0 中 如果没有任何设置 在代码中使用编译指导语句将不会报错 但是也不起作用 1 2 OpenMP发展与优势 1 2 1 OpemMP的规范由SGI发起