范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文
国学影视

OpenMPParallelConstruct实现原理与源码分析

  前言
  在本篇文章当中我们将主要分析 OpenMP 当中的 parallel construct 具体时如何实现的,以及这个 construct 调用了哪些运行时库函数,并且详细分析这期间的参数传递!Parallel 分析——编译器角度
  在本小节当中我们将从编译器的角度去分析该如何处理 parallel construct 。首先从词法分析和语法分析的角度来说这对编译器并不难,只需要加上一些处理规则,关键是编译器将一个 parallel construct 具体编译成了什么?
  下面是一个非常简单的 parallel construct。#pragma omp parallel {   body; }
  编译器在遇到上面的 parallel construct 之后会将代码编译成下面的样子:void subfunction (void *data) {   use data;   body; }  setup data; GOMP_parallel_start (subfunction, &data, num_threads); subfunction (&data); GOMP_parallel_end ();
  首先 parallel construct 中的代码块会被编译成一个函数 sub function,当然了函数名不一定是这个,然后会在使用 #pragma omp parallel 的函数当中将一个 parallel construct 编译成 OpenMP 动态库函数的调用,在上面的伪代码当中也指出了,具体会调用 OpenMP 的两个库函数 GOMP_parallel_start 和 GOMP_parallel_end ,并且主线程也会调用函数 subfunction ,我们在后面的文章当中在仔细分析这两个动态库函数的源代码。深入剖析 Parallel 动态库函数参数传递动态库函数分析
  在本小节当中,我们主要去分析一下在 OpenMP 当中共享参数是如何传递的,以及介绍函数 GOMP_parallel_start 的几个参数的含义。
  首先我们分析函数 GOMP_parallel_start 的参数含义,这个函数的函数原型如下:void GOMP_parallel_start (void (*fn)(void *), void *data, unsigned num_threads)
  上面这个函数一共有三个参数:第一个参数 fn 是一个函数指针,主要是用于指向上面编译出来的 subfunction 这个函数的,因为需要多个线程同时执行这个函数,因此需要将这个函数传递过去,让不同的线程执行。第二个参数是传递的数据,我们在并行域当中会使用到共享的或者私有的数据,这个指针主要是用于传递数据的,我们在后面会仔细分析这个参数的使用。第三个参数是表示 num_threads 子句指定的线程个数,如果不指定这个子句默认的参数是 0 ,但是如果你使用了 IF 子句并且条件是 false 的话,那么这个参数的值就是 1 。这个函数的主要作用是启动一个或者多个线程,并且执行函数 fn 。void GOMP_parallel_end (void)这个函数的主要作用是进行线程的同步,因为一个 parallel 并行域需要等待所有的线程都执行完成之后才继续往后执行。除此之外还需要释放线程组的资源并行返回到之前的 omp_in_parallel() 表示的状态。参数传递分析
  我们现在使用下面的代码来具体分析参数传递过程:#include  #include "omp.h"  int main() {   int data = 100;   int two  = -100;   printf("start "); #pragma omp parallel num_threads(4) default(none) shared(data, two)   {     printf("tid = %d data = %d two = %d ", omp_get_thread_num(), data, two);   }    printf("finished ");   return 0; }
  我们首先来分析一下上面的两个变量 data 和 two 的是如何被传递的,我们首先用图的方式进行表示,然后分析一下汇编程序并且对图进行验证。
  上面的代码当中两个变量 data 和 two 在内存当中的布局结构大致如下所示(假设 data 的初始位置时 0x0):
  那么在函数 GOMP_parallel_start 当中传递的参数 data 就是 0x0 也就是指向 data 的内存地址,如下图所示:
  那么根据上面参数传递的情况,我们就可以在 subfunction 当中使用 *(int*)data 得到 data 的值,使用 *((int*) ((char*)data + 4)) 得到 two 的值,如果是 private 传递的话我们就可以先拷贝这个数据再使用,如果是 shared 的话,那么我们就可以直接使用指针就行啦。
  上面的程序我们用 pthread 大致描述一下,则 pthread 对应的代码如下所示: #include "pthread.h" #include "stdio.h" #include "stdint.h"  typedef struct data_in_main_function{     int data;     int two; }data_in_main_function;  pthread_t threads[4];  void* subfunction(void* data) {   int two = ((data_in_main_function*)data)->two;   int data_ = ((data_in_main_function*)data)->data;   printf("tid = %ld data = %d two = %d ", pthread_self(), data_, two);   return NULL; }  int main() {   // 在主函数申请 8 个字节的栈空间   data_in_main_function data;   data.data = 100;   data.two = -100;   for(int i = 0; i < 4; ++i)   {     pthread_create(&threads[i], NULL, subfunction, &data);   }   for(int i = 0; i < 4; ++i)   {     pthread_join(threads[i], NULL);   }   return 0; }汇编程序分析
  在本节当中我们将仔细去分析上面的程序所产生的汇编程序,在本文当中的汇编程序基础 x86_64 平台。在分析汇编程序之前我们首先需要了解一下 x86函数的调用规约,具体来说就是在进行函数调用的时候哪些寄存器保存函数参数以及是第几个函数参数。具体的规则如下所示:
  寄存器
  含义
  rdi
  第一个参数
  rsi
  第二个参数
  rdx
  第三个参数
  rcx
  第四个参数
  r8
  第五个参数
  r9
  第六个参数
  我们现在仔细分析一下上面的程序的 main 函数的反汇编程序:00000000004006cd 
: 4006cd: 55 push %rbp 4006ce: 48 89 e5 mov %rsp,%rbp 4006d1: 48 83 ec 10 sub $0x10,%rsp 4006d5: c7 45 fc 64 00 00 00 movl $0x64,-0x4(%rbp) 4006dc: c7 45 f8 9c ff ff ff movl $0xffffff9c,-0x8(%rbp) 4006e3: bf f4 07 40 00 mov $0x4007f4,%edi 4006e8: e8 93 fe ff ff callq 400580 4006ed: 8b 45 fc mov -0x4(%rbp),%eax 4006f0: 89 45 f0 mov %eax,-0x10(%rbp) 4006f3: 8b 45 f8 mov -0x8(%rbp),%eax 4006f6: 89 45 f4 mov %eax,-0xc(%rbp) 4006f9: 48 8d 45 f0 lea -0x10(%rbp),%rax 4006fd: ba 04 00 00 00 mov $0x4,%edx 400702: 48 89 c6 mov %rax,%rsi 400705: bf 3d 07 40 00 mov $0x40073d,%edi 40070a: e8 61 fe ff ff callq 400570 40070f: 48 8d 45 f0 lea -0x10(%rbp),%rax 400713: 48 89 c7 mov %rax,%rdi 400716: e8 22 00 00 00 callq 40073d 40071b: e8 70 fe ff ff callq 400590 400720: 8b 45 f0 mov -0x10(%rbp),%eax 400723: 89 45 fc mov %eax,-0x4(%rbp) 400726: 8b 45 f4 mov -0xc(%rbp),%eax 400729: 89 45 f8 mov %eax,-0x8(%rbp) 40072c: bf fa 07 40 00 mov $0x4007fa,%edi 400731: e8 4a fe ff ff callq 400580 400736: b8 00 00 00 00 mov $0x0,%eax 40073b: c9 leaveq 40073c: c3 retq   从上面的反汇编程序我们可以看到在主函数的汇编代码当中确实调用了函数 GOMP_parallel_start 和 GOMP_parallel_end,并且 subfunction 为 main._omp_fn.0 ,它对应的汇编程序如下所示:000000000040073d : 40073d: 55 push %rbp 40073e: 48 89 e5 mov %rsp,%rbp 400741: 48 83 ec 10 sub $0x10,%rsp 400745: 48 89 7d f8 mov %rdi,-0x8(%rbp) 400749: e8 52 fe ff ff callq 4005a0 40074e: 48 8b 55 f8 mov -0x8(%rbp),%rdx 400752: 8b 4a 04 mov 0x4(%rdx),%ecx 400755: 48 8b 55 f8 mov -0x8(%rbp),%rdx 400759: 8b 12 mov (%rdx),%edx 40075b: 89 c6 mov %eax,%esi 40075d: bf 03 08 40 00 mov $0x400803,%edi 400762: b8 00 00 00 00 mov $0x0,%eax 400767: e8 44 fe ff ff callq 4005b0 40076c: c9 leaveq 40076d: c3 retq 40076e: 66 90 xchg %ax,%axGOMP_parallel_start 详细参数分析void (*fn)(void *), 我们现在来看一下函数 GOMP_parallel_start 的第一个参数,根据我们前面谈到的第一个参数应该保存在 rdi 寄存器,我们现在分析一下在 main 函数的反汇编程序当中在调用函数 GOMP_parallel_start 之前 rdi 寄存器的值。我们可以看到在 main 函数位置为 4006f8 的地方的指令 mov $0x40073d,%edi 可以看到 rdi 寄存器的值为 0x40073d (edi 寄存器是 rdi 寄存器的低 32 位),我们可以看到 函数 main._omp_fn.0 的起始地址就是 0x40073d ,因此我们就可以在函数 GOMP_parallel_start 使用这个函数指针了,最终在启动的线程当中调用这个函数。void *data,这是函数 GOMP_parallel_start 的第二个参数,根据前面的分析第二个参数保存在 rsi 寄存器当中,我现在将 main 数当中和 rsi 相关的指令选择出来:00000000004006cd
: 4006cd: 55 push %rbp 4006ce: 48 89 e5 mov %rsp,%rbp 4006d1: 48 83 ec 10 sub $0x10,%rsp 4006d5: c7 45 fc 64 00 00 00 movl $0x64,-0x4(%rbp) 4006dc: c7 45 f8 9c ff ff ff movl $0xffffff9c,-0x8(%rbp) 4006ed: 8b 45 fc mov -0x4(%rbp),%eax 4006f0: 89 45 f0 mov %eax,-0x10(%rbp) 4006f3: 8b 45 f8 mov -0x8(%rbp),%eax 4006f6: 89 45 f4 mov %eax,-0xc(%rbp) 4006f9: 48 8d 45 f0 lea -0x10(%rbp),%rax 400702: 48 89 c6 mov %rax,%rsi   上面的汇编程序的栈空间以及在调用函数之前 GOMP_parallel_start 部分寄存器的指向如下所示:   最终在调用函数 GOMP_parallel_start 之前 rsi 寄存器的指向如上图所示,上图当中 rsi 的指向的内存地址作为参数传递过去。根据上文谈到的 subfunction 中的参数可以知道,在函数 main._omp_fn.0 当中的 rdi 寄存器(也就是第一个参数 *data)的值就是上图当中 rsi 寄存器指向的内存地址的值(事实上也就是 rsi 寄存器的值)。大家可以自行对照着函数 main._omp_fn.0 的汇编程序对 rdi 寄存器的使用就可以知道这其中的参数传递的过程了。unsigned num_threads,根据前文提到的保存第三个参数的寄存器是 rdx,在 main 函数的位置 4006fd 处,指令为 mov $0x4,%edx,这和我们自己写的程序是一致的都是 4 (0x4)。动态库函数源码分析GOMP_parallel_start 源码分析   我们首先来看一下函数 GOMP_parallel_start 的源代码:void GOMP_parallel_start (void (*fn) (void *), void *data, unsigned num_threads) { num_threads = gomp_resolve_num_threads (num_threads, 0); gomp_team_start (fn, data, num_threads, gomp_new_team (num_threads)); }   在这里我们对函数 gomp_team_start 进行分析,其他两个函数 gomp_resolve_num_threads 和 gomp_new_team 只简单进行作用说明,太细致的源码分析其实是没有必要的,感兴趣的同学自行分析即可,我们只需要了解整个执行流程即可。gomp_resolve_num_threads,这个函数的主要作用是最终确定需要几个线程去执行任务,因为我们可能并没有使用 num_threads 子句,而且这个值和环境变量也有关系,因此需要对线程的个数进行确定。gomp_new_team,这个函数的主要作用是创建包含 num_threads 个线程数据的线程组,并且对数据进行初始化操作。gomp_team_start,这个函数的主要作用是启动 num_threads 个线程去执行函数 fn ,这其中涉及一些细节,比如说线程的亲和性(affinity)设置。   由于 gomp_team_start 的源代码太长了,这里只是节选部分源程序进行分析: /* Launch new threads. */ for (; i < nthreads; ++i, ++start_data) { pthread_t pt; int err; start_data->fn = fn; // 这行代码就是将 subfunction 函数指针进行保存最终在函数 gomp_thread_start 当中进行调用 start_data->fn_data = data; // 这里保存函数 subfunction 的函数参数 start_data->ts.team = team; // 线程的所属组 start_data->ts.work_share = &team->work_shares[0]; start_data->ts.last_work_share = NULL; start_data->ts.team_id = i; // 线程的 id 我们可以使用函数 omp_get_thread_num 得到这个值 start_data->ts.level = team->prev_ts.level + 1; start_data->ts.active_level = thr->ts.active_level; #ifdef HAVE_SYNC_BUILTINS start_data->ts.single_count = 0; #endif start_data->ts.static_trip = 0; start_data->task = &team->implicit_task[i]; gomp_init_task (start_data->task, task, icv); team->implicit_task[i].icv.nthreads_var = nthreads_var; start_data->thread_pool = pool; start_data->nested = nested; // 如果使用了线程的亲和性那么还需要进行亲和性设置 if (gomp_cpu_affinity != NULL) gomp_init_thread_affinity (attr); err = pthread_create (&pt, attr, gomp_thread_start, start_data); if (err != 0) gomp_fatal ("Thread creation failed: %s", strerror (err)); }   上面的程序就是最终启动线程的源程序,可以看到这是一个 for 循环并且启动 nthreads 个线程,pthread_create 是真正创建了线程的代码,并且让线程执行函数 gomp_thread_start 可以看到线程不是直接执行 subfunction 而是将这个函数指针保存到 start_data 当中,并且在函数 gomp_thread_start 真正去调用这个函数,看到这里大家应该明白了整个 parallel construct 的整个流程了。   gomp_thread_start 的函数题也相对比较长,在这里我们选中其中的比较重要的几行代码,其余的代码进行省略。对比上面线程启动的 pthread_create 语句我们可以知道,下面的程序真正的调用了 subfunction,并且给这个函数传递了对应的参数。static void * gomp_thread_start (void *xdata) { struct gomp_thread_start_data *data = xdata; /* Extract what we need from data. */ local_fn = data->fn; local_data = data->fn_data; local_fn (local_data); return NULL; }GOMP_parallel_end 分析   这个函数的主要作用就是一个同步点,保证所有的线程都执行完成之后再继续往后执行,这一部分的源代码比较杂,其核心原理就是使用路障 barrier 去实现的,这其中是 OpenMP 自己实现的一个 barrier 而不是直接使用 pthread 当中的 barrier ,这一部分的源程序就不进行仔细分析了,感兴趣的同学可以自行阅读,可以参考 OpenMP 锁实现原理 。总结   在本篇文章当中主要给大家介绍了 parallel construct 的实现原理,以及他的动态库函数的调用以及源代码分析,大家只需要了解整个流程不太需要死扣细节(这并无很大的用处)只有当我们自己需要去实现 OpenMP 的时候需要去了解这些细节,不然我们只需要了解整个动态库的设计原理即可!

郭碧婷婆婆禁声16天,一开口就被网友狂赞向太霸气中国台湾女星郭碧婷6月生下第2胎儿子,和老公向佐凑成好字,让公公向华强和婆婆陈岚(向太)乐不可支,不料向佐上月21日被爆出轨疑云,向太第一时间亲上火线辟谣,隔天也PO文呛声摩羯座一SA联发科和紫光展锐在手机AP出货量上实现两位数增长根据StrategyAnalytics发布的最新研报,在2022年第1季度智能手机应用处理器(applicationsprocessor,AP)的总收入为89亿美元,同比增长了35麦迪近日放狂言,没拿总冠军是没好队友罢了伦纳德怪我咯?麦迪近日表示我可以去世界上任何一个国家,我都能得到认可和尊重,而且非常受尊重。但是想要在NBA夺冠的话,需要非常多的运作,我的打法和我的表现,你觉得给我合适的队友我夺不了冠军吗?很欧阳夏丹主持新闻联播9年,却突然消失,背后有心酸有泪水2020年下半年,有无数的网友们都在疑惑欧阳夏丹去哪了?我爸问我欧阳夏丹去哪了?我外公问我欧阳夏丹去哪了?你是不是好久没见欧阳夏丹主持新闻联播了呢?经此一问,很多人才恍惚发现,是啊暴雪暗黑破坏神4开场实机演示泄露,可自定义角色外观IT之家8月3日消息,暴雪此前已经开放了暗黑破坏神4Beta测试的预注册,还没有公布具体开测时间,但已有越来越多的证据显示测试即将到来。今日,国外传出了一段暗黑破坏神4开场视频,展有甲状腺结节的女性一定要牢记这6吃6不吃,结节越来越小药补不如食补,很多女孩子很贪吃,我记得有一个来找我看病的患者,每年都要来我这里一次,原因是什么呢?说出来让人哭笑不得,她对凉的食物很喜欢,不管是凉菜或者是冷饮,也或者是冰镇水果都是手机被恶意监控了?出现这些状况要警惕很多人都觉得手机被监控离自己很遥远,其实并不是,一般来说,被恶意监控的原因无非就是两种,一种是利益相关,一种是恶趣味。如果你是有一定资产的中产,你可能面临商业对手的监控,如果你是一iOS16beta5新功能电池百分比音乐可视化等!网友真是疯了期待苹果新机的同时,当然也期待着苹果新系统带来的新功能,最近苹果已经向开发人员发布了iOS16beta5,这次增加了什么新玩法呢?电池百分比状态不知道大家是否还记得,当苹果发布带有腾讯与罗技合作的云游戏掌机爆料搭载安卓系统,配置较低IT之家8月3日消息,罗技G官方宣布与腾讯游戏达成合作,今年晚些时候将推出支持多种云游戏服务的云游戏掌机,但未透露其他具体信息。据AYANEO掌机的创始人尾巴大叔爆料,罗技和腾讯合王曼昱世界第一来之不易,巴黎资格竞争激烈,孙颖莎产生掉队危机王曼昱世界第一来之不易,巴黎资格竞争激烈,孙颖莎产生掉队危机,纵观现在的中国乒乓球队,已经完全进入了巴黎奥运周期,但是和东京奥运会周期相比,现在的国乒男女队真是各有烦恼,男队是因为2米41中锋来了!比姚明还高半头,偶像是奥尼尔,NBA球探正在关注NBA漫长休赛期,媒体的报道范围也扩大了不少。就在今天,美国媒体报道,一名尼日利亚小伙,身高已经长到了恐怖的2米41,而且他还是正儿八经的篮球运动员。根据之前的资料,NBA历史上,
苏翊鸣的豪横爸爸,曾放弃生意带小苏玩两年,又一个别人家的爸爸随着苏翊鸣的夺冠,这个17岁夺得奥运冠军,曾经还跨界出演电影的小演员的一度成为新晋顶流,而关于小苏的爸爸妈妈大家也是非常关注,因为大家都很好奇究竟是什么样的父母培养出了这么优秀的儿大爆冷!女单头号种子一轮游,迪亚兹不敌国乒非主力范思琦乒乓球WTT阿曼挑战赛激战正酣,北京时间3月2日晚,女单头号种子选手美洲冠军得主波多黎各名将迪亚兹(世界排名11位)惨遭暴击,在10领先的情况下被翻盘,连丢3局输给了国乒非主力运动又是一年油菜花开时阳春三月,万物复苏,春回大地我们这边的万亩油菜花在春风的沐浴下,竞相开放。吸引了大批游客前来观赏拍照。油菜花可真美啊!远远看去像铺了一块金色的地毯,发出耀眼的光芒,吸引人们来观赏。一喝水,尿就多,这是代表肾脏好,还是不好呢?真相来了医生,我肾脏是不是出了问题,怎么一喝水就想尿。秦大爷是位养生爱好者,每日在朋友圈分享的养生文案数不胜数,前些天在养生群里面两个好友吵了起来,原因很简单,就是喝水多少与肾脏好坏的话题当畸形审美成主流网红撞衫baby被嘲壮士,丰腴体型就是丑?baby在离婚之后过的第一个生日还挺热闹的,一大帮圈内好友集体上前送祝福不说,自己办了场豪华的生日派对。在派对中,她把自己打扮成了迪士尼中的经典人物小叮当(TinkerBell),TFBOYS三人为何只有王俊凯在走下坡路?自从TFBOYS三人出道以来,作为队长的王俊凯一直以来都是人气最高的。然而自从三个人陆陆续续上了大学之后,反而是王俊凯的人气正在下降。其商务广告等和选秀出来的一些艺人几乎等级相同。滚筒洗衣机vs波轮洗衣机,哪个更加实用?随着时代科技的发展,越来越多的居家好物开始不断造福于人们,比如家用洗衣机,从原来的波轮洗衣机,到现在家家户户都在用滚筒洗衣机,除了人们对于价格和外观选择上也是有了很高的要求,对于产自研芯片打造口碑,骁龙8plus加持,OPPO斩获四平台销冠OPPO作为国内本土的老牌手机,一直在国内的手机市场占据一席之地,定期的产品发力更是优势能占据销售榜的榜首,在国内收获了不少的忠实粉丝。随着5G科技浪潮的席卷,数码行业的内卷形式越谢尔巴科娃宣布重要决定!如今在尝试转型当教练,有望来中国执教北京冬奥会女子花样滑冰冠军谢尔巴科娃在近期接受媒体采访时表示,自己现在正在尝试转型为一名专业花滑教练,原因是国际奥委会的决定让她无法再去参加国际比赛,这对于正值巅峰的谢尔巴科娃来说退出花滑世锦赛,中国队这么做对吗?闭门造车或难以进步在花样滑冰这个项目中,中国队的优势不大,这几年女单基本是就只出了一个陈虹伊,男单有金博洋,双人滑则是有隋文静和韩聪在支撑大局。在今年的北京冬奥会中,金博洋的表现非常出色,隋文静和韩伊藤美诚声称要1打5!打败中国夺金,刘国梁安排5朵金花狙击前段时间国乒在打完WTT澳门冠军挑战赛之后,一直在海南这边参加集训。因为就在最近,新加坡这边还会有WTT的大满贯挑战赛,对于我们一众主力来说,在这次取得开门红之后,都非常期待能够在