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

多核编程学习笔记之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 [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函数的时候,可以只引入即可。比如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

你可能感兴趣的:(编程,parallel,thread,编译器,多线程,算法)