OpenMP创建线程中的锁及原子操作性能比较
在多核CPU中锁竞争到底会造成性能怎样的下降呢?相信这是许多人想了解的,因此特地写了一个测试程序来测试原子操作,windows CriticalSection, OpenMP的锁操作函数在多核CPU中的性能。
原子操作选用InterlockedIncrement来进行测试, 对每种锁和原子操作,都测试在单任务执行和多任务执行2000000次加锁解锁操作所消耗的时间。
测试的详细代码见后面。
测试机器环境: Intel 2.66G 双核CPU 机器一台
测试运行结果如下:
SingleThread, InterlockedIncrement 2,000,000: a = 2000000, time = 78 MultiThread, InterlockedIncrement 2,000,000: a = 2000000, time = 156 SingleThread, Critical_Section 2,000,000:a = 2000000, time = 172 MultiThread, Critical_Section, 2,000,000:a = 2000000, time = 3156 SingleThread,omp_lock 2,000,000:a = 2000000, time = 250 MultiThread,omp_lock 2,000,000:a = 2000000, time = 1063
在单任务运行情况下,所消耗的时间如下: 原子操作 78ms Windows CriticalSection 172ms OpenMP 的lock操作 250ms
因此从单任务情况来看,原子操作最快,Windows CriticalSection次之,OpenMP库带的锁最慢,但这几种操作的时间差距不是很大,用锁操作比原子操作慢了2~3倍左右。
在多个任务运行的情况下,所消耗的时间如下:
原子操作 156ms Windows CriticalSection 3156ms OpenMP 的lock操作 1063ms
在多任务运行情况下,情况发生了意想不到的变化,原子操作时间比单任务操作时慢了一倍,在两个CPU上运行比在单个CPU上运行还慢一倍,真是难以想象,估计是任务切换开销造成的。
Windows CriticalSection则更离谱了,居然花了3156ms,是单任务运行时的18倍多的时间,慢得简直无法想象。
OpenMP的lock操作比Windows CriticalSection稍微好一些,但也花了1063ms,是单任务时的7倍左右。
由此可以知道,在多核CPU的多任务环境中,原子操作是最快的,而OpenMP次之,Windows CriticalSection则最慢。
同时从这些锁在单任务和多任务下的性能差距可以看出,,多核CPU上的编程和以往的单核多任务编程会有很大的区别。
需要说明的是,本测试是一种极端情况下的测试,锁住的操作只是一个简单的加1操作,并且锁竞争次数达200万次之多,在实际情况中,一由于任务中还有很多不需要加锁的代码在运行,实际情况中的性能会比本测试的性能好很多。
测试代码如下:
// TestLock.cpp : OpenMP任务中的原子操作和锁性能测试程序。 //
#include
void TestAtomic() {
clock_t t1,t2; int i = 0;
volatile LONG a = 0;
t1 = clock();
for( i = 0; i < 2000000; i++ ) {
InterlockedIncrement( &a); }
t2 = clock();
printf(\= %ld\\n\, a, t2-t1);
t1 = clock();
#pragma omp parallel for
for( i = 0; i < 2000000; i++ ) {
InterlockedIncrement( &a);
}
t2 = clock();
printf(\= %ld\\n\, a, t2-t1); }
void TestOmpLock() {
clock_t t1,t2; int i;
int a = 0;
omp_lock_t mylock;
omp_init_lock(&mylock);
t1 = clock();
for( i = 0; i < 2000000; i++ ) {
omp_set_lock(&mylock); a+=1;
omp_unset_lock(&mylock); }
t2 = clock();
printf(\, a, t2-t1);
t1 = clock();
#pragma omp parallel for
for( i = 0; i < 2000000; i++ ) {
omp_set_lock(&mylock); a+=1;
omp_unset_lock(&mylock); }
t2 = clock();
printf(\, a, t2-t1);
omp_destroy_lock(&mylock); }
void TestCriticalSection() {
clock_t t1,t2; int i;
int a = 0;
CRITICAL_SECTION cs;
InitializeCriticalSection(&cs);
t1 = clock();
for( i = 0; i < 2000000; i++ ) {
EnterCriticalSection(&cs); a+=1;
LeaveCriticalSection(&cs); }
t2 = clock();
printf(\, a, t2-t1);
t1 = clock();
#pragma omp parallel for
for( i = 0; i < 2000000; i++ ) {
EnterCriticalSection(&cs); a+=1;
LeaveCriticalSection(&cs); }
t2 = clock();
printf(\, a, t2-t1);
DeleteCriticalSection(&cs); }
int main(int argc, char* argv[]) {
TestAtomic();
TestCriticalSection(); TestOmpLock();
return 0; }
OpenMP程序设计的两个小技巧 1、动态设置并行循环的线程数量
在实际情况中,程序可能运行在不同的机器环境里,有些机器是双核,有些机器是4核甚至更多核。并且未来硬件存在升级的可能,CPU核数会变得越来越多。如何根据机器硬件的不同来自动设置合适的线程数量就显得很重要了,否则硬件升级后程序就得进行修改,那将是一件很麻烦的事情。
比如刚开始在双核系统中开发的软件,线程数量缺省都设成2,那么当机器升级到4核或8核以后,线程数量就不能满足要求了,除非修改程序。
线程数量的设置除了要满足机器硬件升级的可扩展性外,还需要考虑程序的可扩展性,当程序运算量增加或减少后,设置的线程数量仍然能够满足要求。显然这也不能通过设置静态的线程数量来解决。
在具体计算需要使用多少线程时,主要需要考虑以下两点:
1) 当循环次数比较少时,如果分成过多数量的线程来执行,可能会使得总运行时间高于较
少线程或一个线程执行的情况。并且会增加能耗。
2) 如果设置的线程数量远大于CPU核数的话,那么存在着大量的任务切换和调度等开销,
也会降低整体效率。
那么如何根据循环的次数和CPU核数来动态地设置线程的数量呢?下面以一个例子来说明动态设置线程数量的算法,假设一个需要动态设置线程数的需求为: 1、 以多个线程运行时的每个线程运行的循环次数不低于4次 2、 总的运行线程数最大不超过2倍CPU核数
下面代码便是一个实现上述需求的动态设置线程数量的例子
const int MIN_ITERATOR_NUM = 4;
int ncore = omp_get_num_procs(); //获取执行核的数量
int max_tn = n / MIN_ITERATOR_NUM;
int tn = max_tn > 2*ncore ? 2*ncore : max_tn; //tn表示要设置的线程数量 #pragma omp parallel for if( tn > 1) num_threads(tn) for ( i = 0; i < n; i++ ) {
printf(\, omp_get_thread_num()); //Do some work here }
在上面代码中,根据每个线程运行的循环次数不低于4次,先计算出最大可能的线程数max_tn,然后计算需要的线程数量tn,tn的值等于max_tn和2倍CPU核数中的较小值。
然后在parallel for构造中使用if子句来判断tn是否大于1,大于1时使用单个线程,否则使用tn个线程,,这样就使得设置的线程数量满足了需求中的条件。