C++ 并行加速

一、OpenMP并行加速:

       在MIMD作为主要研究对象的系统中,分为两种类型:共享内存系统分布式内存系统,之前我们介绍的基于MPI方式的并行计算编程是属于分布式内存系统的方式,现在我们研究一种基于OpenMP的共享内存系统的并行编程方法。

VS配置进行OpenMP开发的步骤:

(1)在正式进行OpenMP编码之前,需要对编译器稍微配置一下。启动VS2015新建一个C++的控制台应用程序;

(2)然后在项目解决方案资源管理器上选择项目名称,点击右键,选择“属性”;

(3)然后在属性页上左侧选择“配置属性”——“C/C++”——“语言”,然后在右侧“OpenMP支持”后选择“是(/openmp)” ,这样就能在我们VS2015中进行OpenMP的程序开发了。

在程序中,为了能够使用OpenMP函数,还需要在程序包含omp.h头文件。

#include 

当然也可以使用cmakelists文件进行配置。

但使用它有几天限制条件:

    (1)、OpenMP只能并行化for循环,它不会并行while和do-while循环,而且只能并行循环次数在for循环外面就确定了的for循环。

    (2)、循环变量只能是整型和指针类型(不能是浮点型)

    (3)、循环语句只能是单入口单出口的。循环内部不能改变index,而且里面不能有goto、break、return。但是可以使用continue,因为它并不会减少循环次数。另外exit语句也是可以用的,因为它的能力太大,他一来,程序就结束了。

1.1

#pragma omp parallel for

#pragma omp parallel for是OpenMP中的一个指令,表示接下来的for循环将被多线程执行,另外每次循环之间不能有关系, 如果使用这条简单的指令去运行并行计算的话,程序的线程数将由运行时系统决定(这里使用的算法十分复杂),典型的情况下,系统将在每一个核上运行一个线程。

1.2

如果需要执行使用多少个线程来执行我们的并行程序,就得为parallel指令增加num_threads子句,这样的话,就允许程序员指定执行后代码块的线程数。

#pragma omp parallel num_threads(thread_count)

1.3 

我们注意两个要点:

    (1)OpenMP编译器不检查被parallel for指令并行化的循环所包含的迭代间的依赖关系,而是由程序员来识别这些依赖关系。 

    (2)一个或更多个迭代结果依赖于其它迭代的循环,一般不能被OpenMP正确的并行化

当我们试图使用一个parallel for指令时,需要小心循环依赖关系,而不用担心数据依赖,缺省情况下,任何在循环前声明的变量,在线程间都是共享的。为了消除这种循环依赖关系之外,还需要保证每个线程有它自己的factor副本,OpenMP的语句为我们考虑了,通过添加private子句到parallel指令中来实现这一目标。 

void blog4::Test5(int argc, char* argv[])
{
    double factor = 1;
    double sum = 0.0;
    int n = 1000;
    int thread_count = strtol(argv[1], NULL, 10);//omp_get_num_threads();
    cout << thread_count << endl;
#pragma omp parallel for num_threads(thread_count) reduction(+:sum) private(factor)
    for (int i = 0; i < n; i++)
    {
        if (i % 2 == 0)
            factor = 1.0;
        else
            factor = -1.0;
        sum += factor / (2 * i + 1);
        //factor = -factor;
        //cout << i << "-" << factor << endl;
        //SumForNumber();
    }
    double pi_approx = 4.0*sum;
    printf("%f", pi_approx);
}

1.4

#pragma omp critical    //OpenMP 参考 (同步构造)CRITICAL 指令
  • CRITICAL指令指定一块同一时间只能被一条线程执行的代码区域
  • 注意:

    如果一条线程正在一个CRITICAL区域执行而另一个线程到达这个区域,并企图执行,那么它将会被阻塞,直到第一个线程离开这个区域.
    命名是可选项,使不同的CRITICAL区域共存:
    命名是全局标志符.具有相同命名的不同的CRITICAL区域被当作同一个区域
    所有未命名CRITICAL区域被当作同一个区域.

  • critical的语义是指在任意时刻只有一个(或是最多一个)线程在执行临界区内的代码, 其目的是对于临界区内的线程之间共享的资源进行保护,比如几个线程对一个公共链表进行插入或删除操作,此时就需要使用critical, 否者该链表中的数据可能不一致。在OpenMP中,临界区的名字是可选的,所有的临界区都是外部链接的,也就是说是他们是公共变量,是所有线程可见的,不管该线程属于哪个组。对于没有指定名字的临界区,OpenMP编译时认为所有的没有指定名字的临界区,都对应一个外部(或全局)的临界区。不同于一些常见的OpenMP结构语句,临界区结构的末尾并没有一个隐含的同步路障。临界区的代码应该是一个块结构(structured block), 其中不允许有返回(return), 退出(exit), 或者从里面跳出(go to)语句。
     

    int main()
    {
        int max=0;   
        int a[10]={11,2,33,49,113,20,321,250,689,16};  
        #pragma omp parallel for  
        for(int j=0;j<10;j++)  
        {  
            int temp=a[j];
            printf("temp=a[%d]=%d,id=%d\n",j,a[j],omp_get_thread_num()); 
            #pragma omp critical  
            {  
                if(temp>max)  
                    max=temp;  
                printf("temp=%d,max=%d,id=%d\n",temp,max,omp_get_thread_num());
            }  
        }  
        printf("max=%d\n",max);
        return 0;
    }

    1.5

  • #pragma omp parallel for reduction(+:sum) 

    reduction的语法为recutioin(operator:list)和其他的数据属性子句不一样的是多了一个operator参数。由于最后会进行迭代运算,所以不是所有的运算符都能作为reduction的参数,而且,迭代运算需要一个初始值,不是所有的操作符需要有相同的初始值,一般而言,常见的reduction操作符的初始值为:+(0),*(1),-(0),&~(0),|(0),^(0),&&(1),||(0),当然,这不是必须的,比如叠加运算的初始值,可以是任意值,只是表达的含义不一样而已,但是对于某些操作符,有些初始值是没有什么意义的,比如乘法迭代如果初始值为0没有什么意义,结果肯定是0了!

    典型的使用reduction的例子,就是迭加(求和)操作了:

  • #include   
    #define COUNT 10
     
    int main(int argc, _TCHAR* argv[])  
    {
    	int sum = 100;		// Assign an initial value.
    #pragma omp parallel for reduction(+:sum)
    	for(int i = 0;i < COUNT; i++) 
    	{
    		sum += i;
    	}
    	printf("Sum: %d\n",sum);
     
    	return 0;  
    }

    这个例子就是对0到COUNT进行求和,由于初始值为100,所以还会加一个100,如果只是为了求和,只需要初始值为0即可。使用reduction可以避免数据竞争的发生,将上面例子的COUNT修改为一个比较大的值,如果不使用reduction,会发现有数据竞争导致结果不一致,使用reduction后,每次都能得到正确的结果。

    reduction的使用是比较简单的,主要还是需要理解上面说到的“初始值”,第一个理解是这里的100这样的初始值,这是并行区域外的初始值,会在最后计算到迭代结果中,那么还有一个隐含的初始值,就是我们知道,使用了reduction,那么每个线程都会构造一个reduction变量的线程副本,那么其值为多少呢?从上面的例子可以看出,其初始值就是0,如果初始值都是100,那么结果应该是100会被加线程数目的次数。初始值的确定方法就是上面提到的:+(0),*(1),-(0),&~(0),|(0),^(0),&&(1),||(0)

    所以,理解reduction的工作过程:

    (1)进入并行区域后,team内的每个新的线程都会对reduction变量构造一个副本,比如上面的例子,假设有四个线程,那么,进入并行区域的初始化值分别为:sum0=100,sum1 = sum2 = sum3 = 0.为何sum0为100呢?因为主线程不是一个新的线程,所以不需要再为主线程构造一个副本(没有找到官方这样的说法,但是从理解上,应该就是这样工作的,只会有一个线程使用到并行区域外的初始值,其余的都是0)。

    (2)每个线程使用自己的副本变量完成计算。

    (3)在退出并行区域时,对所有的线程的副本变量使用指定的操作符进行迭代操作,对于上面的例子,即sum' = sum0'+sum1'+sum2'+sum3'.

    (4)将迭代的结果赋值给原来的变量(sum),sum=sum'.

    注意:

    reduction只能用于标量类型(int、float等等);

二、TBB并行加速

你可能感兴趣的:(C++,OpenMP,tbb,并行编程)