基于OpenMP编程模型的多线程程序性能分析
2014-03-26李梅
李梅
(西安欧亚学院 陕西 西安 710065)
多核环境下软件开发的核心是多线程开发[1]。采用多线程程序设计技术可以提高系统及 程序的运行性能,诸如吞吐量、计算速度、响应时间等。所以高性能、高效率是多线程程序并行化的目的之一。但是在很多情况下并行化后的程序并不能达到预期的执行性能。影响执行性能的原因是多方面的,比如OpenMP并行化的开销、线程在 CPU核间的动态迁移、负载平衡、线程同步开销等。
OpenMP是一种面向共享存储体系结构的多线程并行编程语言[2],是一种共享内存并行的应用程序编程接口。所有处理器都被连接到一个共享的内存单元上,处理器在访问内存的时候使用的是相同的内存编址空间,由于内存共享,因此,某一处理器写入的数据会立刻被其他处理器访问到。OpenMP编程模型通过提供一组与平台无关的编译指导、运行时库函数及环境变量,指导编译器何时以及如何利用程序中的并行性进行多线程并行执行。OpenMP在并行执行程序时,采用Fork/Join方式,它的基本思想是串行区域由主线程执行,并行程序通过派生多个线程来并行执行,并行执行的程序要全部结束后才能执行后面的非并行执行的代码[3]。
1 OpenMP并行化的开销
OpenMP是一个外部编程模型,而不是自动编程模型,它能够使程序员完全控制并行化[4]。OpenMP并行化本身是有一定开销的,因为OpenMP获得应用程序多线程并行化能力需要程序库的支持,库中代码的运行会带来一定的开销。这种开销是不可避免的。但有时这种开销是没有必要的。实际上,并不是所有的代码都需要并行化,有些情况下,并行化之后程序的运行效率反而比不上串行执行的效率。很大一部分原因是由于使用OpenMP进行并行化之后引入OpenMP本身的开销过大。因此,只有并行执行代码段负担足够大,而引入OpenMP本身的开销又足够小,此时引入并行化操作才能加速程序的执行。由于并行化会带来额外的开销,因此,从效率上考虑,并不是所有的程序都应当并行化的,特别是对于小程序,并行化带来的效率不足以弥补并行化本身带来的运行负担,勉强进行并行化就会得不偿失。应当尽量使得程序真正工作的负载超过并行化的负担,每一个线程负担的工作要足够多,这样才能获得并行化之后的性能提升。例如:
#include “stdafx.h”
#include
#include
int_tmain(intargc,_TCHAR*argv[])
{
clock_tstart,stop;
unsigned long sum=0;
start=clock();
#pragamomp parallel for reduction(+:sum)
for(int i=0;i<1000;i++)
sum=sum+i;
stop=clock();
printf(“exec with OpenMP:sum=%ul,time=%f seconds ”,sum, ((double)
(stop-start)/1000.0));
sum=0;
start=clock();
for(int i=0;i<1000;i++)
sum=sum+i;
stop=clock();
printf (“serial exec:sum=%ul,time=%f seconds ”,sum,((double)(stop-start)/1000.0));
return 0;
}
第一个循环使用了OpenMP对循环进行并行化,而第二个循环使用了简单的串行执行方式。下面是程序的一次执行结果:
exec with OpenMP:sum=499950001,time=0.016000 seconds serial exec:sum=499950001,time=0.000000 seconds
可以看到串行执行的效率要比并行执行的效率高,这主要是由于循环的规模比较小,使用并行化带来的效果无法抵消并行化的额外负担。但是如果将上述循环次数改为1000000000
exec with openmp:sum=8874597121,timei=0.156000 seconds
serial exec:sum=8874597121,timei=0.297000 seconds
加速比为0.297000/0.156000=1.9034。
从这个例子中明显看到在编写并行化程序时,应当尽量使得程序真正工作的负载超过并行化的负担,每一个线程负担的工作要足够多,这样才能获得并行化之后的性能提升。
2 线程在CPU核间的动态迁移
OpenMP应用程序中,如果过多的线程集中在一个CPU上访问不同的内存块,显然这种对内存总线的竞争会显著降低访存的速度。为提高处理器核的使用效率,主流操作系统调整了其调度算法,最常用的就是负载均衡技术,将 CPU的负荷平均分配到多个 CPU核中,这就意味着,在比较繁忙的CPU核上运行的线程可能会被操作系统自动迁移到空闲的CPU核上,这种迁移将导致被迁移的线程的上下文需要迁移到新的CPU核上。如果频繁迁移会导致应用程序性能下降。为避免线程在CPU核间的动态迁移,可以在不同平台下将OpenMP线程绑定到指定的 CPU核上运行,从而消除由于迁移原因而导致的性能降低。
1)windows平台下线程和CPU核的绑定
一个程序指定到单独一个CPU上运行会比不指定CPU运行时快。这中间主要有两个原因:CPU切换时损耗的性能;Intel的自动降频技术和windows的机制冲突:windows有一个功能是平衡负载,可以将一个线程在不同时间分配到不同CPU,从而使得每一个CPU不“过累”。然而,Inter又有一个技术叫做SpeedStep,当一个CPU没有满负荷运行时自动降频从而达到节能减排的目的。这两个功能实际是冲突的:一个程序被分配到多个CPU协同工作->每个CPU都不是满载->每个CPU都会降频->windows发现每个CPU性能都降低了,因此程序执行速度也降低了。因此,将线程(进程)绑定到指定CPU核心,不让windows自作主张分散任务,从而提高单线程效率是很有必要的。有两种方法实现绑定进程到指定CPU:
手工调节:在资源管理器的进程里面,设置相关性,可以设置进程到某个或者某些指定的CPU核心。
代码自动调节:
DWORD_PTR SetThreadAffinityMask(HANDLE hThread,DWORD_PTR dwThreadAffinityMask);
第一个参数为线程句柄。
第二个参数为 mask,可取值为 0~2^31(32位)和 0~2^63(64位),每一位代表每一个CPU是否使用。
2)Linux平台下线程和CPU核的绑定
从 Linux2.6内核开始,Linux系统提供API函数 sched_setaffinity和sched_getaffinity将线程和CPU核进行绑定。
3 负载均衡
对于OpenMP多线程程序而言,负载均衡是影响其运行性能的重要因素[5]。在多线程程序中,保证线程间的负载平衡是提高程序性能的方法之一。良好的负载平衡可以保证执行核尽可能的在大部分时间里保持忙碌的状态,将调度开销、上下文切换开销和同步开销降到最低。如果负载平衡做的很差,那么某些线程可能很早就完成了自己的工作,从而导致处理器资源闲置,降低了程序执行的性能。
通常情况下,循环并行的负载平衡差是由循环迭代计算时间的不确定性引起的。一方面,有的循环通过检查源代码的方法来确定循环迭代的计算时间是比较容易的。在多数情况下,循环迭代总是耗费一定数量的时间,即便不是这样,也可以找到耗时相近的一组迭代。例如,有时候所有的偶数迭代集合和所有奇数迭代集合所耗费的时间几乎相等,或者循环前半部分迭代和后半部分迭代所耗费的时间几乎相等。另一方面,要找出耗时相同的迭代集合几乎是不可能的。然而不管怎样,都可以通过OpenMP的调度策略提供循环调度信息,使编译器和运行时库能够更好的划分迭代,并将迭代分布到各个线程上,从而实现更好的负载平衡。
在编写OpenMP代码时,注意保证负载的均衡,尽量让每个线程的工作量相当,从而保证程序的执行效率。在循环并行化时,采用将循环次数平均分配到所有线程中的静态分配策略,因此线程的工作量在进入循环并行化之前就已经确定了。这种分配策略在每次循环迭代工作量相仿的时候可以较好的保证线程间的负载平衡,获得良好的执行效率。但是,在实际情况中,每次循环的工作量并不一定相同,有时会差距很大,这时静态分配策略会引起线程间负载的不均衡,使得负载轻的线程无事可做,负载重的线程工作繁忙。
为了解决这个问题,OpenMP提供了动态分配策略,动态策略将循环迭代划分为若干个迭代块,每个块使用一个内部任务队列采用先来先服务的方式进行调度。首先为每个线程各分配一个循环块,当一个线程完成其分配的块后,它将请求另一个循环块,系统将从任务队列头部取出下一个循环块分配给该线程。这个过程不断重复,直至所有的迭代块都被分配执行完成。即让线程根据自己的执行能力向系统申请循环块。动态调度有利于缓解负载不均衡性[6]。
#include"stdafx.h"
#include
#include
void smallwork()
{}
void bigwork()
{unsigned long sum=0;
for(int i=0;i<100000000;i++)sum+=i;
}
int_tmain(intargc, _TCHAR*argv[])
{clock_t start, stop;
start=clock();
#pragma omp parallel for
for(int i=0;i<100;i++){
if(i<50)smallwork();
elsebigwork();
}
stop=clock();
printf ("The first:time=%f seconds ",((double)(stopstart)/1000.0));
start=clock();
#pragma omp parallel for schedule(dynamic,25)
for(int i=0;i<100;i++){
if(i<50)smallwork();
elsebigwork();
}
stop=clock();
printf ("The second:time=%f seconds ",((double)(stopstart)/1000.0));
start=clock();
#pragma omp parallel for
for(int i=0;i<100;i++){
if(i%2)smallwork();
elsebigwork();
}
stop=clock();
printf ("The third:time=%f seconds ",((double)(stopstart)/1000.0));
return 0;
}
下面是某次运行结果:
The first:time=14.859000 seconds
The second:time=8.003000 seconds
The third:time=7.922000 seconds
通过这段代码可以明显看出负载均衡对程序性能的影响。程序中有smallwork()和bigwork()两个函数,分别具有不同的负载,轻载的函数实际上就是一个空函数,而重载的函数则用来求和。
通过执行结果可以看到,虽然三个循环的工作量是一样的,但是运行时间不尽相同。几乎相差了一倍。在第一个循环中,由于步长是1,OpenMP运行时采用静态调度策略将前面50个循环分配给一个线程,将后面50个循环分配给另一个线程。后一个线程需要运行的都是负担沉重的函数,而前一个线程会很快执行完50个空函数,金继续等待另一线程完成工作。在第二个循环中采用那个动态调度策略将循环分为4个迭代块,根据线程的执行情况动态分配,保证线程的负载平衡。在第三个循环处采用修改代码的方法将轻重负载函数均衡地分配给两个线程,从而保证负载平衡。
4 线程同步开销
多个线程在进行同步的时候必然带来一定的同步开销。当然,有的同步开销是不可避免的,但是在某些情况下,不合适的同步机制或者算法会带来运行效率的急剧下降。因此在使用多线程进行应用程序开发时一定要考虑同步的必要性,消除不必要的同步,或者调整同步的顺序,带来性能上的提升。
5 结 论
为提高程序性能,保证程序的执行效率,在编写并行化程序时,应尽量使程序真正工作的负载超过并行化的负担,每个线程负担的工作要足够多;应注意保证负载的平衡,尽量让每个线程的工作量相当;程序开发时一定要考虑同步的必要性,消除不必要的同步。
[1]眭俊华,刘慧娜,王建鑫,等.多核多线程技术综述[J].计算机应用,2013(6):239-242,261.SUIJun-hua,LIUHui-na,WANGJian-xin,etal.Multicore multi-threading technology were reviewed [J].Journal of Computer Applications,2013(6):239-242,261.
[2]于芳.多核平台下的多线程并行编程[J].阴山学刊,2010(9):33-36.YU Fang.Multi-threads parallel programming method on multi-core PC[J].YinshanAcademIc Journal,2010(9):33-36.
[3]何涛,李爱波,黄渊.基于openMP多线程技术SAR地面处理软件的并行设计 [J].计算机工程与应用,2011,47(8):267-271 HE Tao,LI Ai-bo,HUANG Yuan.Parallel designof SAR-ground processing software based on OPenMP[J].Englneering and APPlications,2011,47(8):267-271.
[4]游佐勇.openMP并行编程模型与性能优化方法的研究与应用[D].成都:成都理工大学,2011.
[5]唐玲.openMP多线程负载均衡分析方法及调度策略研究[D].长沙:湖南大学,2010.
[6]任小西,唐玲,李仁发,等.OpenMP多线程负载均衡调度策略研究与实现[J].计算机科学,2010(11):148-151.REN Xiao-xi,TANG Ling,LI Ren-fa,et al.Study and implementation of OpenMP multi-thread load balance scheduling schema[J].Computer Science,2010(11):148-151.