OpenMP task construct实现原理源码分析(openmp,开发技术)

时间:2024-05-10 05:26:29 作者 : 石家庄SEO 分类 : 开发技术
  • TAG :

从编译器角度看 task construct

在本小节当中主要给大家分析一下编译器将 openmp 的 task construct 编译成什么样子,下面是一个 OpenMP 的 task 程序例子:

#include<stdio.h>#include<omp.h>intmain(){#pragmaompparallelnum_threads(4)default(none){#pragmaomptaskdefault(none){printf("HelloWorldfromtid=%d\n",omp_get_thread_num());}}return0;}

首先先捋一下整个程序被编译之后的执行流程,经过前面的文章的学习,我们已经知道了并行域当中的代码会被编译器编译成一个函数,关于这一点我们已经在前面的很多文章当中已经讨论过了,就不再进行复述。事实上 task construct 和 parallel construct 一样,task construct 也会被编译成一个函数,同样的这个函数也会被作为一个参数传递给 OpenMP 内部,被传递的这个函数可能被立即执行,也可能在函数 GOMP_parallel_end 被调用后,在到达同步点之前执行被执行(线程在到达并行域的同步点之前需要保证所有的任务都被执行完成)。

整个过程大致如下图所示:

OpenMP task construct实现原理源码分析

上面的 OpenMP task 程序对应的反汇编程序如下所示:

00000000004008ad<main>:4008ad:55push%rbp4008ae:4889e5mov%rsp,%rbp4008b1:ba04000000mov$0x4,%edx4008b6:be00000000mov$0x0,%esi4008bb:bfdb084000mov$0x4008db,%edi4008c0:e88bfeffffcallq400750<GOMP_parallel_start@plt>4008c5:bf00000000mov$0x0,%edi4008ca:e80c000000callq4008db<main._omp_fn.0>4008cf:e88cfeffffcallq400760<GOMP_parallel_end@plt>4008d4:b800000000mov$0x0,%eax4008d9:5dpop%rbp4008da:c3retq00000000004008db<main._omp_fn.0>:4008db:55push%rbp4008dc:4889e5mov%rsp,%rbp4008df:4883ec10sub$0x10,%rsp4008e3:48897df8mov%rdi,-0x8(%rbp)4008e7:c7042400000000movl$0x0,(%rsp) #参数flags4008ee:41b901000000mov$0x1,%r9d #参数if_clause4008f4:41b801000000mov$0x1,%r8d #参数arg_align4008fa:b900000000mov$0x0,%ecx #参数arg_size4008ff:ba00000000mov$0x0,%edx #参数cpyfn400904:be00000000mov$0x0,%esi #参数data400909:bf15094000mov$0x400915,%edi#这里就是调用函数main._omp_fn.140090e:e89dfeffffcallq4007b0<GOMP_task@plt>400913:c9leaveq400914:c3retq0000000000400915<main._omp_fn.1>:400915:55push%rbp400916:4889e5mov%rsp,%rbp400919:4883ec10sub$0x10,%rsp40091d:48897df8mov%rdi,-0x8(%rbp)400921:e84afeffffcallq400770<omp_get_thread_num@plt>400926:89c6mov%eax,%esi400928:bfd0094000mov$0x4009d0,%edi40092d:b800000000mov$0x0,%eax400932:e849feffffcallq400780<printf@plt>400937:c9leaveq400938:c3retq400939:0f1f8000000000nopl0x0(%rax)

从上面程序反汇编的结果我们可以知道,在主函数当中仍然和之前一样在并行域前后分别调用了 GOMP_parallel_start 和 GOMP_parallel_end,然后在两个函数之间调用并行域的代码 main._omp_fn.0 ,并行域当中的代码被编译成函数 main._omp_fn.0 ,从上面的汇编代码我们可以看到在函数 main._omp_fn.0 调用了函数 GOMP_task ,这个函数的函数声明如下所示:

voidGOMP_task(void(*fn)(void*),void*data,void(*cpyfn)(void*,void*), longarg_size,longarg_align,boolif_clause,unsignedflags);

在这里我们重要解释一下部分参数,首先我们需要了解的是在 x86 当中的函数调用规约:

寄存器含义rdi第一个参数rsi第二个参数rdx第三个参数rcx第四个参数r8第五个参数r9第六个参数

根据上面的寄存器和参数的对应关系,在上面的汇编代码当中已经标注了对应的参数。在这些参数当中最重要的一个参数就是第一个函数指针,对应的汇编语句为 mov $0x400915,%edi,可以看到的是传入的函数的地址为 0x400915,根据上面的汇编程序可以知道这个地址对应的函数就是 main._omp_fn.1,这其实就是 task 区域之间被编译之后的对应的函数,从上面的 main._omp_fn.1 汇编程序当中也可以看出来调用了函数 omp_get_thread_num,这和前面的 task 区域当中代码是相对应的。

现在我们来解释一下其他的几个参数:

  • fn,task 区域被编译之后的函数地址。

  • data,函数 fn 的参数。

  • cpyfn,参数拷贝函数,一般是 NULL,有时候需要 task 当中的数据不能是共享的,需要时私有的,这个时候可能就需要数据拷贝函数,如果有数据需要及进行拷贝而且这个参数还为 NULL 的话,那么在 OpenMP 内部就会使用 memcpy 进行内存拷贝。

  • arg_size,参数的大小。

  • arg_align,参数多少字节对齐。

  • if_clause,if 子句当中的比较结果,如果没有 if 字句的话就是 true 。

  • flags,用于表示 task construct 的特征或者属性,比如是否是最终任务。

我们现在使用另外一个例子,来看看参数传递的变化。

#include<stdio.h>#include<omp.h>intmain(){#pragmaompparallelnum_threads(4)default(none){intdata=omp_get_thread_num();#pragmaomptaskdefault(none)firstprivate(data)if(data>100){data=omp_get_thread_num();printf("data=%dHelloWorldfromtid=%d\n",data,omp_get_thread_num());}}return0;}

上面的程序被编译之后对应的汇编程序如下所示:

00000000004008ad<main>:4008ad:55push%rbp4008ae:4889e5mov%rsp,%rbp4008b1:4883ec10sub$0x10,%rsp4008b5:ba04000000mov$0x4,%edx4008ba:be00000000mov$0x0,%esi4008bf:bfdf084000mov$0x4008df,%edi4008c4:e887feffffcallq400750<GOMP_parallel_start@plt>4008c9:bf00000000mov$0x0,%edi4008ce:e80c000000callq4008df<main._omp_fn.0>4008d3:e888feffffcallq400760<GOMP_parallel_end@plt>4008d8:b800000000mov$0x0,%eax4008dd:c9leaveq4008de:c3retq00000000004008df<main._omp_fn.0>:4008df:55push%rbp4008e0:4889e5mov%rsp,%rbp4008e3:4883ec20sub$0x20,%rsp4008e7:48897de8mov%rdi,-0x18(%rbp)4008eb:e880feffffcallq400770<omp_get_thread_num@plt>4008f0:8945fcmov%eax,-0x4(%rbp)4008f3:837dfc64cmpl$0x64,-0x4(%rbp)4008f7:0f9fc2setg%dl4008fa:8b45fcmov-0x4(%rbp),%eax4008fd:8945f0mov%eax,-0x10(%rbp)400900:488d45f0lea-0x10(%rbp),%rax400904:c7042400000000movl$0x0,(%rsp) #参数flags40090b:4189d1mov%edx,%r9d #参数if_clause40090e:41b804000000mov$0x4,%r8d #参数arg_align400914:b904000000mov$0x4,%ecx #参数arg_size400919:ba00000000mov$0x0,%edx #参数cpyfn40091e:4889c6mov%rax,%rsi #参数data400921:bf2d094000mov$0x40092d,%edi #这里就是调用函数main._omp_fn.1400926:e885feffffcallq4007b0<GOMP_task@plt>40092b:c9leaveq40092c:c3retq000000000040092d<main._omp_fn.1>:40092d:55push%rbp40092e:4889e5mov%rsp,%rbp400931:4883ec20sub$0x20,%rsp400935:48897de8mov%rdi,-0x18(%rbp)400939:488b45e8mov-0x18(%rbp),%rax40093d:8b00mov(%rax),%eax40093f:8945fcmov%eax,-0x4(%rbp)400942:e829feffffcallq400770<omp_get_thread_num@plt>400947:89c2mov%eax,%edx400949:8b45fcmov-0x4(%rbp),%eax40094c:89c6mov%eax,%esi40094e:bff0094000mov$0x4009f0,%edi400953:b800000000mov$0x0,%eax400958:e823feffffcallq400780<printf@plt>40095d:c9leaveq40095e:c3retq40095f:90nop

在上面的函数当中我们将 data 一个 4 字节的数据作为线程私有数据,可以看到给函数 GOMP_task 传递的参数参数的大小以及参数的内存对齐大小都发生来变化,从原来的 0 变成了 4,这因为 int 类型数据占 4 个字节。

Task Construct 源码分析

在本小节当中主要谈论在 OpenMP 内部是如何实现 task 的,关于这一部分内容设计的内容还是比较庞杂,首先需要了解的是在 OpenMP 当中使用 task construct 的被称作显示任务(explicit task),这种任务在 OpenMP 当中会有两个任务队列(双向循环队列),将所有的任务都保存在这样一张列表当中,整体结构如下图所示:

OpenMP task construct实现原理源码分析

在上图当中由同一个线程创建的任务为 child_task,他们之间使用 next_child 和 prev_child 两个指针进行连接,不同线程创建的任务之间可以使用 next_queue 和 prev_queue 两个指针进行连接。

任务的结构体描述如下所示:

structgomp_task{structgomp_task*parent; //任务的父亲任务structgomp_task*children; //子任务structgomp_task*next_child; //下一个子任务structgomp_task*prev_child; //上一个子任务structgomp_task*next_queue; //下一个任务(不一定是同一个线程创建的子任务)structgomp_task*prev_queue; //上一个任务(不一定是同一个线程创建的子任务)structgomp_task_icvicv;//openmp当中内部全局设置使用变量的值(internalcontrolvariable)void(*fn)(void*); //taskconstruct被编译之后的函数void*fn_data; //函数参数enumgomp_task_kindkind;//任务类型具体类型如下面的枚举类型boolin_taskwait; //是否处于taskwait状态boolin_tied_task;//是不是在绑定任务当中boolfinal_task;//是不是最终任务gomp_sem_ttaskwait_sem;//对象锁用于保证线程操作这个数据的时候的线程安全};//openmp当中的任务的状态enumgomp_task_kind{GOMP_TASK_IMPLICIT,GOMP_TASK_IFFALSE,GOMP_TASK_WAITING,GOMP_TASK_TIED};

在了解完上面的数据结构之后我们来看一下前面的给 OpenMP 内部提交任务的函数 GOMP_task,其源代码如下所示:

/*Calledwhenencounteringanexplicittaskdirective.IfIF_CLAUSEisfalse,thenwemustnotdelayinexecutingthetask.IfUNTIEDistrue,thenthetaskmaybeexecutedbyanymemberoftheteam.*/voidGOMP_task(void(*fn)(void*),void*data,void(*cpyfn)(void*,void*), longarg_size,longarg_align,boolif_clause,unsignedflags){structgomp_thread*thr=gomp_thread();//team是OpenMP一个线程组当中共享的数据structgomp_team*team=thr->ts.team;#ifdefHAVE_BROKEN_POSIX_SEMAPHORES/*Ifpthread_mutex_*isusedforomp_*lock*,theneachtaskmustbetiedtoonethreadallthetime.ThismeansUNTIEDtasksmustbetiedandifCPYFNisnon-NULLIF(0)mustbeforced,asCPYFNmightberunningondifferentthreadthanFN.*/if(cpyfn)if_clause=false;if(flags&1)flags&=~1;#endif//这里表示如果是if子句的条件为真的时候或者是孤立任务(team==NULL)或者是最终任务的时候或者任务队列当中的任务已经很多的时候//提交的任务需要立即执行而不能够放入任务队列当中然后在GOMP_parallel_end函数当中进行任务的取出//再执行if(!if_clause||team==NULL||(thr->task&&thr->task->final_task)||team->task_count>64*team->nthreads){structgomp_tasktask;gomp_init_task(&task,thr->task,gomp_icv(false));task.kind=GOMP_TASK_IFFALSE;task.final_task=(thr->task&&thr->task->final_task)||(flags&2);if(thr->task) task.in_tied_task=thr->task->in_tied_task;thr->task=&task;if(__builtin_expect(cpyfn!=NULL,0)) {//这里是进行数据的拷贝 charbuf[arg_size+arg_align-1]; char*arg=(char*)(((uintptr_t)buf+arg_align-1) &~(uintptr_t)(arg_align-1)); cpyfn(arg,data); fn(arg); }else//如果不需要进行数据拷贝则直接执行这个函数 fn(data);/*Accessto"children"isnormallydoneinsideatask_lock mutexregion,buttheonlywaythisparticulartask.children canbesetisifthisthread'staskworkfunction(fn) createschildren.Sosincethesetteris*this*thread,we neednobarriersherewhentestingfornon-NULL.Wecanhave task.childrensetbythecurrentthreadthenchangedbya childthread,butseeingastalenon-NULLvalueisnota problem.Oncepastthetask_lockacquisition,thisthread willseetherealvalueoftask.children.*/if(task.children!=NULL) { gomp_mutex_lock(&team->task_lock); gomp_clear_parent(task.children); gomp_mutex_unlock(&team->task_lock); }gomp_end_task();}else{//下面就是将任务先提交到任务队列当中然后再取出执行structgomp_task*task;structgomp_task*parent=thr->task;char*arg;booldo_wake;task=gomp_malloc(sizeof(*task)+arg_size+arg_align-1);arg=(char*)(((uintptr_t)(task+1)+arg_align-1) &~(uintptr_t)(arg_align-1));gomp_init_task(task,parent,gomp_icv(false));task->kind=GOMP_TASK_IFFALSE;task->in_tied_task=parent->in_tied_task;thr->task=task;//这里就是参数拷贝逻辑如果存在拷贝函数就通过拷贝函数进行参数赋值否则使用memcpy进行//参数的拷贝if(cpyfn) cpyfn(arg,data);else memcpy(arg,data,arg_size);thr->task=parent;task->kind=GOMP_TASK_WAITING;task->fn=fn;task->fn_data=arg;task->in_tied_task=true;task->final_task=(flags&2)>>1;//在这里获取全局队列锁保证下面的代码在多线程条件下的线程安全//因为在下面的代码当中会对全局的队列进行修改操作下面的操作就是队列的一些基本操作啦gomp_mutex_lock(&team->task_lock);if(parent->children) { task->next_child=parent->children; task->prev_child=parent->children->prev_child; task->next_child->prev_child=task; task->prev_child->next_child=task; }else { task->next_child=task; task->prev_child=task; }parent->children=task;if(team->task_queue) { task->next_queue=team->task_queue; task->prev_queue=team->task_queue->prev_queue; task->next_queue->prev_queue=task; task->prev_queue->next_queue=task; }else { task->next_queue=task; task->prev_queue=task; team->task_queue=task; }++team->task_count;gomp_team_barrier_set_task_pending(&team->barrier);do_wake=team->task_running_count+!parent->in_tied_task <team->nthreads;gomp_mutex_unlock(&team->task_lock);if(do_wake) gomp_team_barrier_wake(&team->barrier,1);}}

对于上述所讨论的内容大家只需要了解相关的整体流程即可,细节除非你是 openmp 的开发人员,否则事实上没有多大用,大家只需要了解大致过程即可,帮助你进一步深入理解 OpenMP 内部的运行机制。

但是需要了解的是上面的整个过程还只是将任务提交到 OpenMP 内部的任务队列当中,还没有执行,我们在前面谈到过在线程执行完并行域的代码会执行函数 GOMP_parallel_end 在这个函数内部还会调用其他函数,最终会调用函数 gomp_barrier_handle_tasks 将内部的所有的任务执行完成。

voidgomp_barrier_handle_tasks(gomp_barrier_state_tstate){structgomp_thread*thr=gomp_thread();structgomp_team*team=thr->ts.team;structgomp_task*task=thr->task;structgomp_task*child_task=NULL;structgomp_task*to_free=NULL;//首先对全局的队列结构进行加锁操作gomp_mutex_lock(&team->task_lock);if(gomp_barrier_last_thread(state)){if(team->task_count==0) { gomp_team_barrier_done(&team->barrier,state); gomp_mutex_unlock(&team->task_lock); gomp_team_barrier_wake(&team->barrier,0); return; }gomp_team_barrier_set_waiting_for_tasks(&team->barrier);}while(1){if(team->task_queue!=NULL) { structgomp_task*parent; //从任务队列当中拿出一个任务 child_task=team->task_queue; parent=child_task->parent; if(parent&&parent->children==child_task) parent->children=child_task->next_child; child_task->prev_queue->next_queue=child_task->next_queue; child_task->next_queue->prev_queue=child_task->prev_queue; if(child_task->next_queue!=child_task) team->task_queue=child_task->next_queue; else team->task_queue=NULL; child_task->kind=GOMP_TASK_TIED; team->task_running_count++; if(team->task_count==team->task_running_count) gomp_team_barrier_clear_task_pending(&team->barrier); }gomp_mutex_unlock(&team->task_lock);if(to_free)//释放任务的内存空间to_free在后面会被赋值成child_task { gomp_finish_task(to_free); free(to_free); to_free=NULL; }if(child_task)//调用任务对应的函数 { thr->task=child_task; child_task->fn(child_task->fn_data); thr->task=task; }else return;//退出while循环gomp_mutex_lock(&team->task_lock);if(child_task) { structgomp_task*parent=child_task->parent; if(parent) { child_task->prev_child->next_child=child_task->next_child; child_task->next_child->prev_child=child_task->prev_child; if(parent->children==child_task) { if(child_task->next_child!=child_task) parent->children=child_task->next_child; else { /*Weaccesstask->childreninGOMP_taskwait outsideofthetasklockmutexregion,so needareleasebarrierheretoensurememory writtenbychild_task->fnaboveisflushed beforetheNULLiswritten.*/ __atomic_store_n(&parent->children,NULL, MEMMODEL_RELEASE); if(parent->in_taskwait) gomp_sem_post(&parent->taskwait_sem); } } } gomp_clear_parent(child_task->children); to_free=child_task; child_task=NULL; team->task_running_count--; if(--team->task_count==0 &&gomp_team_barrier_waiting_for_tasks(&team->barrier)) { gomp_team_barrier_done(&team->barrier,state); gomp_mutex_unlock(&team->task_lock); gomp_team_barrier_wake(&team->barrier,0); gomp_mutex_lock(&team->task_lock); } }}}
 </div> <div class="zixun-tj-product adv-bottom"></div> </div> </div> <div class="prve-next-news">
本文:OpenMP task construct实现原理源码分析的详细内容,希望对您有所帮助,信息来源于网络。
上一篇:Python如何实现搜索Google Scholar论文信息下一篇:

15 人围观 / 0 条评论 ↓快速评论↓

(必须)

(必须,保密)

阿狸1 阿狸2 阿狸3 阿狸4 阿狸5 阿狸6 阿狸7 阿狸8 阿狸9 阿狸10 阿狸11 阿狸12 阿狸13 阿狸14 阿狸15 阿狸16 阿狸17 阿狸18