1、背景介绍
- 硬件和隔核设置:
Intel E5 V4 14核。
配置 isolcpus=2,3,4,5,6,7,8,9,10,11,12,13,隔离了 12 个核心,仅保留核心 0 和核心 1 作为普通调度核心。
- 操作系统
湖南麒麟3.3-3B
- OpenMP并行配置:
使用核心 4 到核心 13(10 个核心)运行 OpenMP 并行线程。
- 调度策略:
主进程使用 FIFO 调度策略。
- 编译器和运行时:
ICC 2015.1 编译的程序能够动态切换。
GCC 4.8.5编译的程序并行度切换时存在问题
2、现象描述
针对不同的编译器,采用同一份测试程序,来验证并行计算情况,源码如下
/*============================================================================Name : pthread_test.cAuthor : Version :Copyright : Your copyright noticeDescription : Hello World in C, Ansi-style============================================================================*/#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>
#include <error.h>
#include <malloc.h>
#include <netdb.h>
#include <netinet/in.h>
#include <assert.h>
#include <fcntl.h>
#include <getopt.h>
#include <stdint.h>
#include <string.h>
#include <time.h>
#include <unistd.h>#include <sys/stat.h>
#include <sys/types.h>
#include <sys/select.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/mman.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <arpa/inet.h>#include <sched.h>
#include <omp.h>#define SIZE 1024
double a[2048][2048]={0};
double b[2048][2048]={0};
double result[2048][2048]={0};int hello1()
{int iLoop =0;int i = 0;int j=0;int k=0;#pragma omp parallel num_threads(10){int thread_id=omp_get_thread_num();int cpu_id=4+ thread_id;cpu_set_t cpuset;CPU_ZERO(&cpuset);CPU_SET(cpu_id,&cpuset);printf("###########CPU CORE %d\n",cpu_id);if( 0 != pthread_setaffinity_np(pthread_self(),sizeof(cpu_set_t),&cpuset)){printf("pthread:set affinity failed\n");exit(EXIT_FAILURE);}}#pragma omp parallel for num_threads(10)for(i=0;i<SIZE;i++){for(j=0;j<SIZE;j++){double sum=0.0;for(k=0;k<SIZE;k++){sum+=a[i][k]*b[k][j];}result[i][j]=sum;}}#pragma omp parallel for num_threads(3)for(i=0;i<SIZE;i++){for(j=0;j<SIZE;j++){double sum=0.0;for(k=0;k<SIZE;k++){sum+=a[i][k]*b[k][j];}result[i][j]=sum;}}#pragma omp parallel for num_threads(10)for(i=0;i<SIZE;i++){for(j=0;j<SIZE;j++){double sum=0.0;for(k=0;k<SIZE;k++){sum+=a[i][k]*b[k][j];}result[i][j]=sum;}}return 0;
}int main()
{int ret = 0;int i = 0,j=0;cpu_set_t cpu_info;pthread_t xdma_t1[5];pthread_attr_t attr1[5];for(i=0;i<SIZE;i++){for(j=0;j<SIZE;j++){a[i][j]=sin(i+j);b[i][j]=cos(i-j);result[i][j]=0.0;}}struct sched_param param;param.sched_priority = 10;if(sched_setscheduler(getpid(), SCHED_RR, ¶m) == -1){printf("sched_setscheduler() failed!\n");return -1;}if(0 != pthread_create(&xdma_t1,NULL,(void*)hello1,NULL) ){printf("FILE:%s LINE:%d FUNC: CmpDsPCtrl Error!\n",__FILE__,__LINE__);return -1;}else{printf("%s %d Cmp: CmpDsPCtrl Started!\n",__FILE__,__LINE__);}while(1){sleep(1);}return;
}
上述代码中创建了10个核来并行计算,并行计算内容为矩阵乘法,并行度也设计为可动态切换的,即一开始是10线程并行,后面变成3线程并行,最后恢复10线程并行。
ICC编译器运行效果如下:
可以看到ICC编译器编译后的程序能够实现并行度调整,而且在并行度从10变成3的时候线程并没有减少。
GCC编译器运行效果如下:
可以看到,GCC编译器编译后的程序并行度从10变成3时线程自动消失了,后续并行度再提升到10时,增加的线程全部被放到了第一个核上,导致该核心被任务占满,并没有实现10核多线程并行。
3、问题排查
首先,在隔核的情况下,采用FIFO调度策略能将openmp线程调度到隔离核上,这一点与普通线程不一样,普通线程需要配置OMP_PLACE=cores环境变量才行,这一点在chatgpt上面有说明
为什么 FIFO 线程可以在隔离核心上运行?
- FIFO 调度策略与普通调度策略的不同:
- FIFO(先进先出)是实时调度策略之一。使用 FIFO 调度策略的线程通常拥有较高的调度优先级,并且会在系统中被优先调度。
- 默认情况下,普通的进程调度会根据 isolcpus 设置避免在隔离核心上运行非实时线程。但实时线程(例如使用 FIFO 或其他实时调度策略的线程)通常会被允许在这些隔离的核心上运行,前提是该核心并未完全被其他重要进程或中断占用。
- 隔离核心与实时线程:
- 即使核心被 isolcpus 隔离,这些核心仍然可以为实时线程提供计算资源。系统不会主动地将实时线程调度到非隔离核心。
- 由于 FIFO 线程具有较高的优先级,并且实时线程不会被常规调度器干扰,因此它们能够在隔离核心上运行。
- OpenMP 与实时线程的交互:
- OpenMP 是一种共享内存并行编程模型,它依赖于底层的线程库(如 pthreads)来管理并行线程的创建和调度。默认情况下,OpenMP 会由普通线程管理,并且其线程调度依赖于操作系统的标准调度器。
- 当您通过 FIFO 调度策略创建线程时,这些线程由于高优先级,可能会影响 OpenMP 线程的调度,使其能够在隔离核心上执行。具体而言,当 OpenMP 线程创建时,它们的调度被实时线程干扰或优先级提升,可能会导致 OpenMP 线程成功在隔离核心上运行。
为什么 OpenMP 能在 FIFO 线程的帮助下运行?
在隔离核心上运行 OpenMP 并行任务的过程,可能是因为您通过 FIFO 线程干预了线程的调度,导致 OpenMP 线程能够在隔离核心上运行。具体来说:
- 实时线程的干预:FIFO 线程在高优先级下运行时,可能会“抢占”调度资源,迫使 OpenMP 线程也在隔离核心上执行。这种方式与 OpenMP 默认的调度行为不同,通常情况下,OpenMP 线程只能运行在非隔离核心上。
- 线程的绑定机制:FIFO 线程被创建时,系统可能会自动将其绑定到隔离的核心上,并确保它们有足够的计算资源。OpenMP 线程也可能继承了这一绑定,或者由于 FIFO 线程的干预,成功地在隔离核心上运行。
如何通过 FIFO 线程确保 OpenMP 并行运行在隔离核心上?
- 使用 FIFO 线程创建 OpenMP 线程: 如果你想要明确地确保 OpenMP 线程在隔离核心上运行,可以在 FIFO 线程中创建 OpenMP 线程并进行亲和性设置。例如,可以通过 pthread_setaffinity_np 设置 FIFO 线程绑定到隔离核心,并确保 OpenMP 线程继承该绑定。
其次,openmp库是编译器自带的,ICC使用的和GCC的不一样
- ICC 2015.1:使用 Intel 提供的 OpenMP 运行时库(
libiomp5
)。
- 优点:Intel 的运行时库经过高度优化,在 Intel 硬件上有更好的性能(如线程绑定和负载平衡)。
- 缺点:在非 Intel 硬件上可能没有显著优势。
- GCC 4.8.5:使用 GNU 提供的 OpenMP 运行时库(
libgomp
)。
- 优点:更通用的实现,在各种硬件上兼容性较好。
- 缺点:在性能优化上通常不如 Intel 的实现。
最后,ICC和GCC对Openmp线程池的管理方式不一样,如下:
这个问题的根本原因是 ICC 和 GCC 对于 OpenMP 线程池的管理机制不同。具体来说:
1. OpenMP 线程池管理的差异
ICC:
- ICC 的 OpenMP 运行时库 (
libiomp5
) 会维持一个全局的线程池,线程池中的线程在程序运行期间不会轻易销毁。- 当并行区域(
#pragma omp parallel
)的线程数量减少时(例如从 10 减少到 3),多余的线程会被放回线程池,但不会被销毁。这些线程会保持空闲状态,以便后续并行区域使用。- 优点:减少线程的创建和销毁开销,适合频繁进入和退出并行区域的场景。
- 缺点:多余线程仍然会占用资源(如栈空间、内存)。
GCC:
- GCC 的 OpenMP 运行时库 (
libgomp
) 在处理线程时更加灵活。当并行区域的线程数减少时,多余的线程会直接被销毁。- 优点:减少资源占用,尤其在线程数频繁变化的情况下。
- 缺点:频繁销毁和重新创建线程可能增加开销,尤其是在短时间内多次进入并行区域时。
2. 线程绑定和调度机制的不同
ICC:
- 在代码中使用了
pthread_setaffinity_np
明确绑定线程到特定的 CPU 核心。这种绑定策略可能导致线程即使处于空闲状态,也不会从系统中移除,因为 ICC 的运行时库需要维护这些绑定关系。- ICC 的运行时库会优先尝试保持线程的绑定,以优化线程与 CPU 缓存的亲和性。
GCC:
- GCC 通常不主动维护线程的绑定关系,多余的线程可以被更容易地销毁。
- 如果未明确设置线程亲和性,多余的线程可能被直接释放,回收系统资源。
3. OpenMP 环境变量的影响
ICC 和 GCC 的运行时库对以下环境变量的处理不同:
OMP_THREAD_LIMIT
:限制 OpenMP 的最大线程数。ICC 更倾向于尊重这个限制,即使减少线程,多余线程也会保持空闲状态。OMP_WAIT_POLICY
:
- 在 ICC 中,如果设置为
PASSIVE
,线程可能进入休眠状态,但不会被销毁。- 在 GCC 中,即使设置为
PASSIVE
,线程也可能被销毁。KMP_AFFINITY
(ICC 特有):控制线程的绑定行为。即使线程数减少,ICC 运行时库仍然可能保留线程以确保绑定关系。
4. 线程生命周期的实现细节
- ICC:
- 线程池的实现会复用已经创建的线程,不会轻易销毁。
- 如果线程绑定到特定 CPU 核心,这种绑定关系会保持,即使线程处于空闲状态。
- GCC:
- 在线程数减少时,多余的线程会被回收,释放资源。
- 线程销毁后,绑定关系会自动解除。
总结
- ICC 编译器在 OpenMP 的实现中,更加注重性能优化和线程的重用。即使减少线程,多余线程也会保持空闲状态,以减少后续并行区域的创建开销。
- GCC 编译器的实现更注重资源管理,减少不必要的线程占用,但可能在频繁并行区域切换时增加开销。
如果希望 ICC 的行为与 GCC 更接近,可以考虑调整以下设置:
- 使用环境变量
KMP_BLOCKTIME=0
,使空闲线程立即进入休眠状态。- 调整
OMP_WAIT_POLICY=PASSIVE
,减少线程的占用资源。- 避免设置过于严格的线程绑定策略,允许更多的灵活性。
总结:针对INTEL 处理器,还是用ICC来编译Openmp并行计算的程序效果更好,能最大限度的使用CPU资源,实现高效并行计算。