OpenMP中任务调度主要针对并行的for循环,当循环中每次迭代的计算量不相等时,如果简单地给各个线程分配相同次数的迭代,则可能会造成各个线程计算负载的不平衡,影响程序的整体性能。
如下面的代码中,如果每个线程执行的任务数量平均分配,有的线程会结束早,有的线程结束晚:
1 #include2 #include 3 4 int main(){ 5 int a[100][100] = {0}; 6 #pragma omp parallel for 7 for (int i =0; i < 100; i++){ 8 for(int j = i; j < 100; j++ ) 9 a[i][j] = ((i%7)*(j%13)%23); 10 } 11 return 0; 12 }
为此,OpenMP提供了schedule子句来实现任务的调度。
schedule子句:
schedule(type[, size]),
参数type是指调度的类型,可以取值为static,dynamic,guided,runtime四种值。其中runtime允许在运行时确定调度类型,因此实际调度策略只有前面三种。
参数size表示每次调度的迭代数量,必须是整数。该参数是可选的。当type的值是runtime时,不能够使用该参数。
1.静态调度static
大部分编译器在没有使用schedule子句的时候,默认是static调度。static在编译的时候就已经确定了,那些循环由哪些线程执行。
当不使用size 时,将给每个线程分配┌N/t┐个迭代。当使用size时,将每次给线程分配size次迭代。
如下面代码:
1 #include2 #include 3 int main(){ 4 int a[100][100] = {0}; 5 #pragma omp parallel for schedule(static) 6 //#pragma omp parallel for schedule(static,5) 7 for (int i =0; i < 100; i++){ 8 printf("id=%d i=%d\n",omp_get_thread_num(),i); 9 } 10 return 0; 11 }
在四核机器上执行:
(1)当不使用参数时,100/4=25,0-24由1号线程执行;25-49由2号线程执行;50-74由3号线程执行;75-99由4号线程执行
(1)当不使用参数时,x(x=0,1,2,3)线程执行((n/5)%4)任务。其中n=0-99。
2.动态调度dynamic
动态调度依赖于运行时的状态动态确定线程所执行的迭代,也就是线程执行完已经分配的任务后,会去领取还有的任务。由于线程启动和执行完的时间不确定,所以迭代被分配到哪个线程是无法事先知道的。
当不使用size 时,是将迭代逐个地分配到各个线程。当使用size 时,逐个分配size个迭代给各个线程。
如下面代码:
1 #include2 #include 3 int main(){ 4 int a[100][100] = {0}; 5 #pragma omp parallel for schedule(dynamic) 6 //#pragma omp parallel for schedule(dynamic,5) 7 for (int i =0; i < 100; i++){ 8 printf("id=%d i=%d\n",omp_get_thread_num(),i); 9 } 10 return 0; 11 }
3.启发式调度guided
采用启发式调度方法进行调度,每次分配给线程迭代次数不同,开始比较大,以后逐渐减小。
size表示每次分配的迭代次数的最小值,由于每次分配的迭代次数会逐渐减少,少到size时,将不再减少。如果不知道size的大小,那么默认size为1,即一直减少到1。具体采用哪一种启发式算法,需要参考具体的编译器和相关手册的信息。
三种运行方式总结:
静态调度static:每次哪些循环由那个线程执行时固定的,编译调试。由于每个线程的任务是固定的,但是可能有的循环任务执行快,有的慢,不能达到最优。
动态调度dynamic:根据线程的执行快慢,已经完成任务的线程会自动请求新的任务或者任务块,每次领取的任务块是固定的。
启发式调度guided:每个任务分配的任务是先大后小,指数下降。当有大量任务需要循环时,刚开始为线程分配大量任务,最后任务不多时,给每个线程少量任务,可以达到线程任务均衡。