OpenMP Parallel Construct 实现原理与源码分析

news2024/10/7 12:20:02

OpenMP Parallel Construct 实现原理与源码分析

前言

在本篇文章当中我们将主要分析 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 <stdio.h>
#include "omp.h"

int main()
{
  int data = 100;
  int two  = -100;
  printf("start\n");
#pragma omp parallel num_threads(4) default(none) shared(data, two)
  {
    printf("tid = %d data = %d two = %d\n", omp_get_thread_num(), data, two);
  }

  printf("finished\n");
  return 0;
}

我们首先来分析一下上面的两个变量 data 和 two 的是如何被传递的,我们首先用图的方式进行表示,然后分析一下汇编程序并且对图进行验证。

上面的代码当中两个变量 datatwo 在内存当中的布局结构大致如下所示(假设 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\n", 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 <main>:
  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 <puts@plt>
  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 <GOMP_parallel_start@plt>
  40070f:       48 8d 45 f0             lea    -0x10(%rbp),%rax
  400713:       48 89 c7                mov    %rax,%rdi
  400716:       e8 22 00 00 00          callq  40073d <main._omp_fn.0>
  40071b:       e8 70 fe ff ff          callq  400590 <GOMP_parallel_end@plt>
  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 <puts@plt>
  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 <main._omp_fn.0>:
  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 <omp_get_thread_num@plt>
  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 <printf@plt>
  40076c:       c9                      leaveq 
  40076d:       c3                      retq   
  40076e:       66 90                   xchg   %ax,%ax

GOMP_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 <main>:
  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 的时候需要去了解这些细节,不然我们只需要了解整个动态库的设计原理即可!


更多精彩内容合集可访问项目:https://github.com/Chang-LeHung/CSCore

关注公众号:一无是处的研究僧,了解更多计算机(Java、Python、计算机系统基础、算法与数据结构)知识。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/179350.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

逆序遍历List集合

1 问题手写一个程序&#xff0c;完成List集合对象的逆序遍历2 方法创建List接口的多态对象向创建好list集合添加元素使用hasPrevious方法。import java.util.ArrayList;import java.util.List;import java.util.ListIterator;public class TestList { public static void ma…

如何好好说话第11章 攀登抽象之梯

在心里面放一把抽象之梯我们要时刻去概括。从更宏观的角度去理解我们当前所处的事情。抽上之梯的这个概念&#xff0c;在写作教材中常常出现。他指引我们写作的时候&#xff0c;不要站在梯子的中间。要么具体详实&#xff0c;要么抽象而精简短小。为什么不要站在梯子中间呢&…

蓝桥杯 stm32 MCP4017

本文代码使用 HAL 库。 文章目录前言一、MCP4017 的重要特性二、MCP4017 计算 RBW 阻值三、MCP4017 地址四、MCP4017 读写函数五、CubeMX 创建工程 &#xff08;利用 ADC 测量 MCP4017 电压&#xff09;、对应代码&#xff1a;总结前言 一、MCP4017 的重要特性 蓝桥杯 板子上…

冯诺依曼体系结构及操作系统(OS)的简单认识

文章目录冯诺依曼体系结构操作系统&#xff08;Operator System&#xff09;冯诺依曼体系结构 冯诺依曼结构也称普林斯顿结构&#xff0c;是一种将程序指令存储器和数据存储器合并在一起的存储结构。数学家冯诺依曼提出了计算机制造的三个基本原则&#xff0c;即采用二进制逻辑…

虚拟机的介绍及安装

文章目录虚拟机介绍VMware WorkStation安装在VMware上安装Linux远程连接Linux系统虚拟机介绍 通过虚拟化技术&#xff0c;在电脑内&#xff0c;虚拟出计算机硬件&#xff0c;并给虚拟的硬件安装操作系统&#xff0c;即可得到一台虚拟的电脑&#xff0c;称之为虚拟机。 VMware…

Ubuntu安装wordpress

这里写自定义目录标题开始环境安装打开参考链接开始 环境 这里安装的php是7.4版本&#xff0c; apt install apache2 php mariadb-server apt install php7.4-mysql php-dev记得需要单独安装php7.4-mysql&#xff0c;不然可能会报错连接数据库出错&#xff0c;中电是wp_chec…

(机械师T90外接显卡GTX-1080)Win10笔记本通过M.2接口外接独立显卡+解决错误代码43

文章目录前言一、硬件清单二、硬件安装1.插入转接卡2.显卡安装3.接入电源4.连接显示器三、驱动安装&#xff08;重点&#xff09;1.禁用笔记本独显2.卸载驱动3.安装新驱动4.解决错误代码43外接显卡使用体验前言 据外接显卡成功也快有一个月了&#xff0c;期间畅玩了刺客信条奥…

【C进阶】程序环境和预处理

⭐博客主页&#xff1a;️CS semi主页 ⭐欢迎关注&#xff1a;点赞收藏留言 ⭐系列专栏&#xff1a;C语言进阶 ⭐代码仓库&#xff1a;C Advanced 家人们更新不易&#xff0c;你们的点赞和关注对我而言十分重要&#xff0c;友友们麻烦多多点赞&#xff0b;关注&#xff0c;你们…

新年快到了,如何运用Python实现春联

这篇博文可能写晚了,现在已经是初五了,哈哈!小编也是最近才想到的,看到老爸去年二十九(或者三十)在写春联,当时觉得他写的好慢啊!(不过让我来写,肯定不行!),于是萌生了用小编所学知识实现春联,只不过家里面没有打印机,不然代码写完之后,之后直接用打印机打印出来就…

【汤姆猫的实现 Objective-C语言】

一、实现之后的效果 1.左边3个按钮,分别是 1)点击之后,汤姆猫掏出一只白色的小鸟,刚要吃,它飞走了, 2)汤姆猫放屁 3)汤姆猫喝牛奶, 右边3个按钮,分别是 1)汤姆猫用爪子把屏幕抓出3个道子, 2)汤姆猫用披萨饼砸屏幕, 3)汤姆猫拿出两个锣,对着一敲, 2.这…

Java 23种设计模式(7.结构型模式-装饰者模式)

结构型模式-装饰者模式 代码分析 类图 代码 abstract class Component {abstract void operation(); }public class ConcreteComponent extends Component {void operation() {System.out.println("具体类");} }public class Decorator extends Component{private …

为向IPv6过渡的组织发布安全指南

美国国家安全局 (NSA) 已发布指南&#xff0c;以帮助国防部 (DoD) 和其他系统管理员识别和减轻与过渡到互联网协议版本 6 (IPv6) 相关的网络风险。 IPv6 由互联网工程任务组 (IETF) 开发&#xff0c;是用于识别和定位系统并在互联网上路由流量的协议的最新版本&#xff0c;提供…

统计学习方法 学习笔记(5)决策树

决策树5.1.决策树模型与学习5.2.特征选择5.3.决策树的生成5.4.决策树的剪枝5.5.CART算法决策树基本概述&#xff1a; 算法类别&#xff1a;一种基本的分类和回归方法&#xff1b;基本结构&#xff1a;呈现树形结构&#xff0c;在分类问题中表示基于特征对实例进行分类的过程。…

云原生之使用docker部署Postgresql数据库

云原生之使用docker部署Postgresql数据库一、Postgresql介绍1.PostgreSQL简介2.PostgreSQL的特点二、检查本地docker环境1.检查系统版本2.检查docker版本3.检查docker状态三、下载Postgresql镜像四、部署Postgresql数据库1.创建Postgresql容器2.查看Postgresql容器状态3.查看Po…

通配符的匹配很全面, 但无法找到元素 ‘context:component-scan‘ 的声明。

HTTP状态 500 - 内部服务器错误 类型 异常报告 消息 Servlet[springMVC]的Servlet.init&#xff08;&#xff09;引发异常 描述 服务器遇到一个意外的情况&#xff0c;阻止它完成请求。 例外情况 javax.servlet.ServletException: Servlet[springMVC]的Servlet.init&#x…

一文看懂Linux基础

文章目录什么是Linux操作系统window和linux的对比快照的拍摄Linux的文件结构kali Linux的简单介绍基础命令vim命令文件权限kali的常见工具查看命令1.查看操作&#xff1a;2.删除操作&#xff1a;3.复制操作&#xff1a;4.移动操作&#xff1a;5.重命名操作&#xff1a;7.上传文…

【时间复杂度和空间复杂度】

1.时间复杂度时间复杂度的定义&#xff1a;在计算机科学中&#xff0c;算法的时间复杂度是一个数学函数&#xff0c;它定量描述了该算法的额外运行时间。一个算法执行所耗费的时间&#xff0c;从理论上说&#xff0c;是不能算出来的&#xff0c;只有你把你的程序放在机器上跑起…

Linux系统用户和权限

文章目录root用户用户、用户组管理查看权限控制修改权限控制 - chmod修改权限控制 - chownroot用户 无论是Windows、MacOS、Linux均采用多用户的管理模式进行权限管理。 在Linux系统中&#xff0c;拥有最大权限的账户名为&#xff1a;root&#xff08;超级管理员&#xff09; …

机器学习(七):Azure机器学习模型搭建实验

文章目录 Azure机器学习模型搭建实验 前言 Azure平台简介 Azure机器学习实验 Azure机器学习模型搭建实验 前言 了解Azure机器学习平台&#xff0c;知道机器学习流程。 Azure平台简介 Azure Machine Learning&#xff08;简称“AML”&#xff09;是微软在其公有云Azure上推…

前缀树 字典树 TrieTree的学习与模拟实现

目录前言前缀树介绍C实现核心思想前缀树的大致框架前缀树插入字符串前缀树查找完整的字符串前缀树查找前缀匹配的字符串前缀树删除完整字符串总结完整代码前言 哥们在去年12月的一次实习面试的时候&#xff0c;远在旧金山的一家美企CTO面试我&#xff0c;岗位在西安&#xff0…