前言 在本篇文章当中我们将主要分析OpenMP当中的parallelconstruct具体时如何实现的,以及这个construct调用了哪些运行时库函数,并且详细分析这期间的参数传递!Parallel分析编译器角度 在本小节当中我们将从编译器的角度去分析该如何处理parallelconstruct。首先从词法分析和语法分析的角度来说这对编译器并不难,只需要加上一些处理规则,关键是编译器将一个parallelconstruct具体编译成了什么? 下面是一个非常简单的parallelconstruct。pragmaompparallel{} 编译器在遇到上面的parallelconstruct之后会将代码编译成下面的样子:voidsubfunction(voiddata){}GOMPparallelstart(subfunction,data,numthreads);subfunction(data);GOMPparallelend(); 首先parallelconstruct中的代码块会被编译成一个函数subfunction,当然了函数名不一定是这个,然后会在使用pragmaompparallel的函数当中将一个parallelconstruct编译成OpenMP动态库函数的调用,在上面的伪代码当中也指出了,具体会调用OpenMP的两个库函数GOMPparallelstart和GOMPparallelend,并且主线程也会调用函数subfunction,我们在后面的文章当中在仔细分析这两个动态库函数的源代码。深入剖析Parallel动态库函数参数传递动态库函数分析 在本小节当中,我们主要去分析一下在OpenMP当中共享参数是如何传递的,以及介绍函数GOMPparallelstart的几个参数的含义。 首先我们分析函数GOMPparallelstart的参数含义,这个函数的函数原型如下:voidGOMPparallelstart(void(fn)(void),voiddata,unsignednumthreads) 上面这个函数一共有三个参数:第一个参数fn是一个函数指针,主要是用于指向上面编译出来的subfunction这个函数的,因为需要多个线程同时执行这个函数,因此需要将这个函数传递过去,让不同的线程执行。第二个参数是传递的数据,我们在并行域当中会使用到共享的或者私有的数据,这个指针主要是用于传递数据的,我们在后面会仔细分析这个参数的使用。第三个参数是表示numthreads子句指定的线程个数,如果不指定这个子句默认的参数是0,但是如果你使用了IF子句并且条件是false的话,那么这个参数的值就是1。这个函数的主要作用是启动一个或者多个线程,并且执行函数fn。voidGOMPparallelend(void)这个函数的主要作用是进行线程的同步,因为一个parallel并行域需要等待所有的线程都执行完成之后才继续往后执行。除此之外还需要释放线程组的资源并行返回到之前的ompinparallel()表示的状态。参数传递分析 我们现在使用下面的代码来具体分析参数传递过程:includestdio。hincludeomp。hintmain(){intdata100;inttwo100;printf(start);pragmaompparallelnumthreads(4)default(none)shared(data,two){printf(tidddatadtwod,ompgetthreadnum(),data,two);}printf(finished);return0;} 我们首先来分析一下上面的两个变量data和two的是如何被传递的,我们首先用图的方式进行表示,然后分析一下汇编程序并且对图进行验证。 上面的代码当中两个变量data和two在内存当中的布局结构大致如下所示(假设data的初始位置时0x0): 那么在函数GOMPparallelstart当中传递的参数data就是0x0也就是指向data的内存地址,如下图所示: 那么根据上面参数传递的情况,我们就可以在subfunction当中使用(int)data得到data的值,使用((int)((char)data4))得到two的值,如果是private传递的话我们就可以先拷贝这个数据再使用,如果是shared的话,那么我们就可以直接使用指针就行啦。 上面的程序我们用pthread大致描述一下,则pthread对应的代码如下所示:includepthread。hincludestdio。hincludestdint。htypedefstructdatainmainfunction{}pthreadtthreads〔4〕;voidsubfunction(voiddata){inttwo((datainmainfunction)data)intdata((datainmainfunction)data)printf(tidlddatadtwod,pthreadself(),data,two);returnNULL;}intmain(){在主函数申请8个字节的栈空间data。data100;data。two100;for(inti0;i4;i){pthreadcreate(threads〔i〕,NULL,subfunction,data);}for(inti0;i4;i){pthreadjoin(threads〔i〕,NULL);}return0;}汇编程序分析 在本节当中我们将仔细去分析上面的程序所产生的汇编程序,在本文当中的汇编程序基础x8664平台。在分析汇编程序之前我们首先需要了解一下x86函数的调用规约,具体来说就是在进行函数调用的时候哪些寄存器保存函数参数以及是第几个函数参数。具体的规则如下所示: 寄存器 含义 rdi 第一个参数 rsi 第二个参数 rdx 第三个参数 rcx 第四个参数 r8 第五个参数 r9 第六个参数 我们现在仔细分析一下上面的程序的main函数的反汇编程序:00000000004006cdmain:4006cd:55pushrbp4006ce:4889e5movrsp,rbp4006d1:4883ec10sub0x10,rsp4006d5:c745fc64000000movl0x64,0x4(rbp)4006dc:c745f89cffffffmovl0xffffff9c,0x8(rbp)4006e3:bff4074000mov0x4007f4,edi4006e8:e893feffffcallq400580putsplt4006ed:8b45fcmov0x4(rbp),eax4006f0:8945f0moveax,0x10(rbp)4006f3:8b45f8mov0x8(rbp),eax4006f6:8945f4moveax,0xc(rbp)4006f9:488d45f0lea0x10(rbp),rax4006fd:ba04000000mov0x4,edx400702:4889c6movrax,rsi400705:bf3d074000mov0x40073d,edi40070a:e861feffffcallq400570GOMPparallelstartplt40070f:488d45f0lea0x10(rbp),rax400713:4889c7movrax,rdi400716:e822000000callq40073dmain。ompfn。040071b:e870feffffcallq400590GOMPparallelendplt400720:8b45f0mov0x10(rbp),eax400723:8945fcmoveax,0x4(rbp)400726:8b45f4mov0xc(rbp),eax400729:8945f8moveax,0x8(rbp)40072c:bffa074000mov0x4007fa,edi400731:e84afeffffcallq400580putsplt400736:b800000000mov0x0,eax40073b:c9leaveq40073c:c3retq 从上面的反汇编程序我们可以看到在主函数的汇编代码当中确实调用了函数GOMPparallelstart和GOMPparallelend,并且subfunction为main。ompfn。0,它对应的汇编程序如下所示:000000000040073dmain。ompfn。0:40073d:55pushrbp40073e:4889e5movrsp,rbp400741:4883ec10sub0x10,rsp400745:48897df8movrdi,0x8(rbp)400749:e852feffffcallq4005a0ompgetthreadnumplt40074e:488b55f8mov0x8(rbp),rdx400752:8b4a04mov0x4(rdx),ecx400755:488b55f8mov0x8(rbp),rdx400759:8b12mov(rdx),edx40075b:89c6moveax,esi40075d:bf03084000mov0x400803,edi400762:b800000000mov0x0,eax400767:e844feffffcallq4005b0printfplt40076c:c9leaveq40076d:c3retq40076e:6690xchgax,axGOMPparallelstart详细参数分析void(fn)(void),我们现在来看一下函数GOMPparallelstart的第一个参数,根据我们前面谈到的第一个参数应该保存在rdi寄存器,我们现在分析一下在main函数的反汇编程序当中在调用函数GOMPparallelstart之前rdi寄存器的值。我们可以看到在main函数位置为4006f8的地方的指令mov0x40073d,edi可以看到rdi寄存器的值为0x40073d(edi寄存器是rdi寄存器的低32位),我们可以看到函数main。ompfn。0的起始地址就是0x40073d,因此我们就可以在函数GOMPparallelstart使用这个函数指针了,最终在启动的线程当中调用这个函数。voiddata,这是函数GOMPparallelstart的第二个参数,根据前面的分析第二个参数保存在rsi寄存器当中,我现在将main数当中和rsi相关的指令选择出来:00000000004006cdmain:4006cd:55pushrbp4006ce:4889e5movrsp,rbp4006d1:4883ec10sub0x10,rsp4006d5:c745fc64000000movl0x64,0x4(rbp)4006dc:c745f89cffffffmovl0xffffff9c,0x8(rbp)4006ed:8b45fcmov0x4(rbp),eax4006f0:8945f0moveax,0x10(rbp)4006f3:8b45f8mov0x8(rbp),eax4006f6:8945f4moveax,0xc(rbp)4006f9:488d45f0lea0x10(rbp),rax400702:4889c6movrax,rsi 上面的汇编程序的栈空间以及在调用函数之前GOMPparallelstart部分寄存器的指向如下所示: 最终在调用函数GOMPparallelstart之前rsi寄存器的指向如上图所示,上图当中rsi的指向的内存地址作为参数传递过去。根据上文谈到的subfunction中的参数可以知道,在函数main。ompfn。0当中的rdi寄存器(也就是第一个参数data)的值就是上图当中rsi寄存器指向的内存地址的值(事实上也就是rsi寄存器的值)。大家可以自行对照着函数main。ompfn。0的汇编程序对rdi寄存器的使用就可以知道这其中的参数传递的过程了。unsignednumthreads,根据前文提到的保存第三个参数的寄存器是rdx,在main函数的位置4006fd处,指令为mov0x4,edx,这和我们自己写的程序是一致的都是4(0x4)。动态库函数源码分析GOMPparallelstart源码分析 我们首先来看一下函数GOMPparallelstart的源代码:voidGOMPparallelstart(void(fn)(void),voiddata,unsignednumthreads){numthreadsgompresolvenumthreads(numthreads,0);gompteamstart(fn,data,numthreads,gompnewteam(numthreads));} 在这里我们对函数gompteamstart进行分析,其他两个函数gompresolvenumthreads和gompnewteam只简单进行作用说明,太细致的源码分析其实是没有必要的,感兴趣的同学自行分析即可,我们只需要了解整个执行流程即可。gompresolvenumthreads,这个函数的主要作用是最终确定需要几个线程去执行任务,因为我们可能并没有使用numthreads子句,而且这个值和环境变量也有关系,因此需要对线程的个数进行确定。gompnewteam,这个函数的主要作用是创建包含numthreads个线程数据的线程组,并且对数据进行初始化操作。gompteamstart,这个函数的主要作用是启动numthreads个线程去执行函数fn,这其中涉及一些细节,比如说线程的亲和性(affinity)设置。 由于gompteamstart的源代码太长了,这里只是节选部分源程序进行分析:Launchnewthreads。for(;i,startdata){这行代码就是将subfunction函数指针进行保存最终在函数gompthreadstart当中进行调用这里保存函数subfunction的函数参数startdatats。线程的所属组startdatats。workshareteamworkshares〔0〕;startdatats。lastworkshareNULL;startdatats。线程的id我们可以使用函数ompgetthreadnum得到这个值startdatats。levelteamprevts。level1;startdatats。activelevelthrts。ifdefHAVESYNCBUILTINSstartdatats。singlecount0;endifstartdatats。statictrip0;startdatataskteamimplicittask〔i〕;gompinittask(startdatatask,task,icv);teamimplicittask〔i〕。icv。如果使用了线程的亲和性那么还需要进行亲和性设置if(gompcpuaffinity!NULL)gompinitthreadaffinity(attr);errpthreadcreate(pt,attr,gompthreadstart,startdata);if(err!0)gompfatal(Threadcreationfailed:s,strerror(err));} 上面的程序就是最终启动线程的源程序,可以看到这是一个for循环并且启动nthreads个线程,pthreadcreate是真正创建了线程的代码,并且让线程执行函数gompthreadstart可以看到线程不是直接执行subfunction而是将这个函数指针保存到startdata当中,并且在函数gompthreadstart真正去调用这个函数,看到这里大家应该明白了整个parallelconstruct的整个流程了。 gompthreadstart的函数题也相对比较长,在这里我们选中其中的比较重要的几行代码,其余的代码进行省略。对比上面线程启动的pthreadcreate语句我们可以知道,下面的程序真正的调用了subfunction,并且给这个函数传递了对应的参数。staticvoidgompthreadstart(voidxdata){Extractwhatweneedfromdata。localfn(localdata);returnNULL;}GOMPparallelend分析 这个函数的主要作用就是一个同步点,保证所有的线程都执行完成之后再继续往后执行,这一部分的源代码比较杂,其核心原理就是使用路障barrier去实现的,这其中是OpenMP自己实现的一个barrier而不是直接使用pthread当中的barrier,这一部分的源程序就不进行仔细分析了,感兴趣的同学可以自行阅读,可以参考OpenMP锁实现原理。总结 在本篇文章当中主要给大家介绍了parallelconstruct的实现原理,以及他的动态库函数的调用以及源代码分析,大家只需要了解整个流程不太需要死扣细节(这并无很大的用处)只有当我们自己需要去实现OpenMP的时候需要去了解这些细节,不然我们只需要了解整个动态库的设计原理即可!