在计算机体系结构中,并行度是指指令并行执行的最大条数。在设计并行程序时,我们可以简单地把并行度认为是在多核/众核处理器上能同时执行的线程数/进程数。对于同一个程序,并行度设计方法的不同将会严重影响到程序的性能。MIC上的并行度优化主要涉及并行线程/进程的数目、并行层级、并行粒度等方面。
1 并行度
MIC卡包含众多的物理核,同时每个核上可以开启4个线程,因此,程序员只有设计足够多的线程/进程才可以把所有的核利用起来。例如一块60个核的MIC卡上,我们最多可以开启240个线程,最佳线程数一般是每个核设置3个或4个线程,图1展示的是某一实际高性能应用程序在60个核的MIC卡上设置不同线程数的性能扩展性结果图,从该图可以看出,只有让MIC卡上的所有核都充分利用起来才能发挥MIC的最大性能。当然,也不是在MIC卡上设置的线程数越多越好,线程数太多的话,线程开销比较大,我们只需要设置的线程数可以保证程序并发度和MIC核的高利用率即可。
图1 某一高性能应用程序在MIC上的性能扩展性
2并行粒度
并行程序是否选择了合适的层级上实现并行,是性能优化中需要关心的重要问题。根据并行程序尽可能使用粗粒度的并行原则,尽可能在最上层并行化代码。在外层上并行除了带来易编程的好处之外,还可以带来好的性能:增加粒度,减少了线程调度和销毁的次数,也就是减少了线程本身的开销所占的比例,尤其对于MIC平台要开启上百个线程,减少线程的开启对性能影响更为重要;同时,隐藏了底层的线程交互,减少了不必要的同步带来的损耗。
下面通过简单的例子说明并行层级,例如程序中有两层循环,并且每层循环都没有数据依赖性,即两层循环都可以并行,根据并行程序尽可能使用粗粒度的并行原则我们可以采用在i层循环并行的方式。
1. #pragma omp parallel for num_threads(THREAD_NUM) 2. for (i=0; i<M; i++) 3. { 4. for (j=0; j<N; j++) 5. { 6. … 7. } 8. } |
当然,并不是所有的应用程序都是在外层循环并行效果最佳,外层循环的并行可能会导致线程之间访问的数据跨度比较大,可能会引起Cache miss,这种情况下可能采取内层循环的并行效果更佳,同时为了减少线程的开销,我们可以在外层for之前开启多线程,在内层for进行任务分发,如上面的代码采用下面的并行方式。
1 #pragma omp parallel num_threads(THREAD_NUM) 2 for (i=0; i<M; i++) 3 { 4 #pragma omp for 5 for (j=0; j<N; j++) 6 { 7 … 8 } 9 } |
在实际的应用程序中也可能出现某一层循环无法达到MIC的并行度要求,针对这种情况,我们可以采取多层循环合并的方式。例如上面的代码中M=20,N=30,无论我们并行哪层for都无法达到MIC的并行度要求,我们可以合并两层for,合并之后的循环次数为600次,显然可以满足MIC平台上的要求。当然,我们也可以采用嵌套并行的方式满足MIC的并行度要求。
合并循环:
1 #pragma omp parallel for num_threads(THREAD_NUM) 2 for (k=0; k<M*N; k++) 3 { 4 i = k/M; 5 j = k%M; 6 … 7 } |
嵌套并行:
1 omp_set_nested(true); //允许嵌套并行 2 #pragma omp parallel for num_threads(THREAD_NUM1) 3 for (i=0; i<M; i++) 4 { 5 #pragma omp parallel for num_threads(THREAD_NUM2) 6 for (j=0; j<N; j++) 7 { 8 … 9 } 10 } |