关于openmp:OpenMP-task-construct-实现原理以及源码分析

OpenMP task construct 实现原理以及源码剖析前言在本篇文章当中次要给大家介绍在 OpenMP 当中 task 的实现原理,以及他调用的相干的库函数的具体实现。在本篇文章当中最重要的就是了解整个 OpenMP 的运行机制。 从编译器角度看 task construct在本大节当中次要给大家剖析一下编译器将 openmp 的 task construct 编译成什么样子,上面是一个 OpenMP 的 task 程序例子: #include <stdio.h>#include <omp.h>int main(){#pragma omp parallel num_threads(4) default(none) {#pragma omp task default(none) { printf("Hello World from tid = %d\n", omp_get_thread_num()); } } return 0;}首先先捋一下整个程序被编译之后的执行流程,通过后面的文章的学习,咱们曾经晓得了并行域当中的代码会被编译器编译成一个函数,对于这一点咱们曾经在后面的很多文章当中曾经探讨过了,就不再进行复述。事实上 task construct 和 parallel construct 一样,task construct 也会被编译成一个函数,同样的这个函数也会被作为一个参数传递给 OpenMP 外部,被传递的这个函数可能被立刻执行,也可能在函数 GOMP_parallel_end 被调用后,在达到同步点之前执行被执行(线程在达到并行域的同步点之前须要保障所有的工作都被执行实现)。整个过程大抵如下图所示: 下面的 OpenMP task 程序对应的反汇编程序如下所示: 00000000004008ad <main>: 4008ad: 55 push %rbp 4008ae: 48 89 e5 mov %rsp,%rbp 4008b1: ba 04 00 00 00 mov $0x4,%edx 4008b6: be 00 00 00 00 mov $0x0,%esi 4008bb: bf db 08 40 00 mov $0x4008db,%edi 4008c0: e8 8b fe ff ff callq 400750 <GOMP_parallel_start@plt> 4008c5: bf 00 00 00 00 mov $0x0,%edi 4008ca: e8 0c 00 00 00 callq 4008db <main._omp_fn.0> 4008cf: e8 8c fe ff ff callq 400760 <GOMP_parallel_end@plt> 4008d4: b8 00 00 00 00 mov $0x0,%eax 4008d9: 5d pop %rbp 4008da: c3 retq00000000004008db <main._omp_fn.0>: 4008db: 55 push %rbp 4008dc: 48 89 e5 mov %rsp,%rbp 4008df: 48 83 ec 10 sub $0x10,%rsp 4008e3: 48 89 7d f8 mov %rdi,-0x8(%rbp) 4008e7: c7 04 24 00 00 00 00 movl $0x0,(%rsp) # 参数 flags 4008ee: 41 b9 01 00 00 00 mov $0x1,%r9d # 参数 if_clause 4008f4: 41 b8 01 00 00 00 mov $0x1,%r8d # 参数 arg_align 4008fa: b9 00 00 00 00 mov $0x0,%ecx # 参数 arg_size 4008ff: ba 00 00 00 00 mov $0x0,%edx # 参数 cpyfn 400904: be 00 00 00 00 mov $0x0,%esi # 参数 data 400909: bf 15 09 40 00 mov $0x400915,%edi # 这里就是调用函数 main._omp_fn.1 40090e: e8 9d fe ff ff callq 4007b0 <GOMP_task@plt> 400913: c9 leaveq 400914: c3 retq0000000000400915 <main._omp_fn.1>: 400915: 55 push %rbp 400916: 48 89 e5 mov %rsp,%rbp 400919: 48 83 ec 10 sub $0x10,%rsp 40091d: 48 89 7d f8 mov %rdi,-0x8(%rbp) 400921: e8 4a fe ff ff callq 400770 <omp_get_thread_num@plt> 400926: 89 c6 mov %eax,%esi 400928: bf d0 09 40 00 mov $0x4009d0,%edi 40092d: b8 00 00 00 00 mov $0x0,%eax 400932: e8 49 fe ff ff callq 400780 <printf@plt> 400937: c9 leaveq 400938: c3 retq 400939: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)从下面程序反汇编的后果咱们能够晓得,在主函数当中依然和之前一样在并行域前后别离调用了 GOMP_parallel_start 和 GOMP_parallel_end,而后在两个函数之间调用并行域的代码 main.\_omp\_fn.0 ,并行域当中的代码被编译成函数 main.\_omp\_fn.0 ,从下面的汇编代码咱们能够看到在函数 main.\_omp\_fn.0 调用了函数 GOMP_task ,这个函数的函数申明如下所示: ...

March 5, 2023 · 9 min · jiezi

关于openmp:OpenMP-Sections-Construct-实现原理以及源码分析

OpenMP Sections Construct 实现原理以及源码剖析前言在本篇文章当中次要给大家介绍 OpenMP 当中次要给大家介绍 OpenMP 当中 sections construct 的实现原理以及他调用的动静库函数剖析。如果曾经理解过了后面的对于 for 的调度形式的剖析,本篇文章就非常简单了。 编译器角度剖析在这一大节当中咱们将从编译器角度去剖析编译器会怎么解决 sections construct ,咱们以上面的 sections construct 为例子,看看编译器是如何解决 sections construct 的。 #pragma omp sections{ #pragma omp section stmt1; #pragma omp section stmt2; #pragma omp section stmt3;}下面的代码会被编译器转换成上面的模式,其中 GOMP_sections_start 和 GOMP_sections_next 是并发平安的,他们都会返回一个数据表示第几个 omp section 代码块,其中 GOMP_sections_start 的参数是示意有几个 omp section 代码块,并且返回给线程一个整数示意线程须要执行第几个 section 代码块,这两个函数的意义不同的是在 GOMP_sections_start 当中会进行一些数据的初始化操作。当两个函数返回 0 的时候示意所有的 section 都被执行完了,从而退出 for 循环。 for (i = GOMP_sections_start (3); i != 0; i = GOMP_sections_next ()) switch (i) { case 1: stmt1; break; case 2: stmt2; break; case 3: stmt3; break; }GOMP_barrier ();动静库函数剖析事实上在函数 GOMP_sections_start 和函数 GOMP_sections_next 当中调用的都是咱们之前剖析过的函数 gomp_iter_dynamic_next ,这个函数实际上就是让线程始终原子指令去竞争数据块(chunk),这个特点和 sections 须要实现的语意是雷同的,只不过 sections 的块大小(chunk size)都是等于 1 的,因为一个线程一次只可能执行一个 section 代码块。 ...

February 16, 2023 · 3 min · jiezi

关于openmp:OpenMP-Parallel-Construct-实现原理与源码分析

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 的几个参数的含意。 ...

January 25, 2023 · 5 min · jiezi

关于openmp:深入理解-OpenMP-线程同步机制

深刻了解 OpenMP 线程同步机制前言在本篇文章当中次要给大家介绍 OpenMP 当中线程的同步和互斥机制,在 OpenMP 当中次要有三种不同的线程之间的互斥形式: 应用 critical 子句,应用这个子句次要是用于创立临界区和 OpenMP 提供的运行时库函数的作用是统一的,只不过这种办法是间接通过编译领导语句实现的,更加不便一点,加锁和解锁的过程编译器会帮咱们实现。应用 atomic 指令,这个次要是通过原子指令,次要是有处理器提供的一些原子指令实现的。OpenMP 给咱们提供了 omp_lock_t 和 omp_nest_lock_t 两种数据结构实现简略锁和可重入锁。在本篇文章当中次要探讨 OpenMP 当中的互斥操作,在下一篇文章当中次要探讨 OpenMP 当中原子操作的实现原理,并且查看程序编译之后的汇编指令。 自定义线程之间的同步 barrier在理论的写程序的过程当中咱们可能会有一种需要就是须要期待所有的线程都执行实现之才可能进行前面的操作,这个时候咱们就能够本人应用 barrier 来实现这个需要了。 比方咱们要实现上面的一个计算式: $$data = \frac{1! + 2! + ... + n!}{n}$$ 当初咱们计算 n = 16 的时候下面的表达式的值: #include <stdio.h>#include <omp.h>int factorial(int n){ int s = 1; for(int i = 1; i <= n; ++i) { s *= i; } return s;}int main(){ int data[16];#pragma omp parallel num_threads(16) default(none) shared(data) { int id = omp_get_thread_num(); data[id] = factorial(id + 1); // 期待下面所有的线程都实现的阶乘的计算#pragma omp barrier long sum = 0;#pragma omp single { for(int i = 0; i < 16; ++i) { sum += data[i]; } printf("final value = %lf\n", (double) sum / 16); } } return 0;}在下面的代码当中咱们首先让 16 个线程都计算实现对应的阶乘后果之后而后在求和进行除法操作,因而在进行除法操作之前就须要将所有的阶乘计算实现,在这里咱们就能够应用 #pragma omp barrier 让所有的线程达到这个同步点之后才持续实现后执行,这样就保障了在进行前面的工作的时候所有线程计算阶乘的工作曾经实现。 ...

January 21, 2023 · 8 min · jiezi

关于openmp:Openmp-Runtime-库函数汇总下深入剖析锁原理与实现

Openmp Runtime 库函数汇总(下)——深刻分析锁原理与实现前言在本篇文章当中次要给大家介绍一下 OpenMP 当中常常应用到的锁并且仔细分析它其中的外部原理!在 OpenMP 当中次要有两种类型的锁,一个是 omp_lock_t 另外一个是 omp_nest_lock_t,这两个锁的次要区别就是后者是一个可重入锁,所谓可冲入锁就是一旦一个线程曾经拿到这个锁了,那么它下一次想要拿这个锁的就是就不会阻塞,然而如果是 omp_lock_t 不论一个线程是否拿到了锁,只有以后锁没有开释,不论哪一个线程都不可能拿到这个锁。在后问当中将有认真的例子来解释这一点。本篇文章是基于 GNU OpenMP Runtime Library ! 深入分析 omp_lock_t这是 OpenMP 头文件给咱们提供的一个构造体,咱们来看一下它的定义: typedef struct{ unsigned char _x[4] __attribute__((__aligned__(4)));} omp_lock_t;事实上这个构造体并没有什么特地的就是占 4 个字节,咱们甚至能够认为他就是一个 4 字节的 int 的类型的变量,只不过应用形式有所差别。与这个构造体相干的次要有以下几个函数: omp_init_lock,这个函数的次要性能是初始化 omp_lock_t 对象的,当咱们初始化之后,这个锁就处于一个没有上锁的状态,他的函数原型如下所示:void omp_init_lock(omp_lock_t *lock);omp_set_lock,在调用这个函数之前肯定要先调用函数 omp_init_lock 将 omp_lock_t 进行初始化,直到这个锁被开释之前这个线程会被始终阻塞。如果这个锁被以后线程曾经获取过了,那么将会造成一个死锁,这就是下面提到了锁不可能重入的问题,而咱们在前面将要剖析的锁 omp_nest_lock_t 是可能进行重入的,即便以后线程曾经获取到了这个锁,也不会造成死锁而是会从新取得锁。这个函数的函数原型如下所示:void omp_set_lock(omp_lock_t *lock);omp_test_lock,这个函数的次要作用也是用于获取锁,然而这个函数可能会失败,如果失败就会返回 false 胜利就会返回 true,与函数 omp_set_lock 不同的是,这个函数并不会导致线程被阻塞,如果获取锁胜利他就会立刻返回 true,如果失败就会立刻返回 false 。它的函数原型如下所示:int omp_test_lock(omp_lock_t *lock); omp_unset_lock,这个函数和下面的函数对应,这个函数的次要作用就是用于解锁,在咱们调用这个函数之前,必须要应用 omp_set_lock 或者 omp_test_lock 获取锁,它的函数原型如下:void omp_unset_lock(omp_lock_t *lock);omp_destroy_lock,这个办法次要是对锁进行回收解决,然而对于这个锁来说是没有用的,咱们在后文剖析他的具体的实现的时候会发现这是一个空函数。咱们当初应用一个例子来具体的体验一下下面的函数: #include <stdio.h>#include <omp.h>int main(){ omp_lock_t lock; // 对锁进行初始化操作 omp_init_lock(&lock); int data = 0;#pragma omp parallel num_threads(16) shared(lock, data) default(none) { // 进行加锁解决 同一个时刻只可能有一个线程可能获取锁 omp_set_lock(&lock); data++; // 解锁解决 线程在出临界区之前须要解锁 好让其余线程可能进入临界区 omp_unset_lock(&lock); } omp_destroy_lock(&lock); printf("data = %d\n", data); return 0;}在下面的函数咱们定义了一个 omp_lock_t 锁,并且在并行域内启动了 16 个线程去执行 data ++ 的操作,因为是多线程环境,因而咱们须要将下面的操作进行加锁解决。 ...

January 16, 2023 · 6 min · jiezi