首頁 > 軟體

OpenMP中For Construct對dynamic的排程方式詳解

2023-02-05 14:01:18

前言

在本篇文章當中主要給大家介紹 OpenMp for construct 的實現原理,以及與他相關的動態庫函數分析,與 for construct 非常相關的是迴圈的排程方式,在 OpenMP 當中一共有四種調調方式,auto, dynamic, guided, runtime, 在本篇文章當中主要是對 dynamic 的排程方式進行分析。

前置知識

在介紹 for construct 的實現原理之前,我們首先需要了解一下編譯器是如何處理常式引數傳遞的(本文基於 x86_64 ISA),我們來看一下下面的程式碼在編譯之後函數引數的傳遞情況。

在前面的文章當中我們已經談到過了,在 x86 當中引數傳遞的規約,具體的內容如下所示:

暫存器含義
rdi第一個引數
rsi第二個引數
rdx第三個引數
rcx第四個引數
r8第五個引數
r9第六個引數

我們現在使用下面的程式碼來分析一下具體的情況(因為前面使用暫存器只能夠傳遞 6 個引數,而在後面我們要分析的動態庫函數當中會傳遞 7 個引數,因此這裡我們使用 8 個引數來測試一下具體的引數傳遞情況):

#include "stdio.h"
 
void echo(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8)
{
  printf("%d %d %d %d %d %d %d %dn", a8, a7, a1, a2, a3, a4, a5, a6);
}
 
int main()
{
  echo(1, 2, 3, 4 ,5 ,6, 7, 8);
  return 0;
}

上面的程式的反組合結果如下所示:

000000000040053d <echo>:
  40053d:       55                      push   %rbp
  40053e:       48 89 e5                mov    %rsp,%rbp
  400541:       48 83 ec 30             sub    $0x30,%rsp
  400545:       89 7d fc                mov    %edi,-0x4(%rbp)
  400548:       89 75 f8                mov    %esi,-0x8(%rbp)
  40054b:       89 55 f4                mov    %edx,-0xc(%rbp)
  40054e:       89 4d f0                mov    %ecx,-0x10(%rbp)
  400551:       44 89 45 ec             mov    %r8d,-0x14(%rbp)
  400555:       44 89 4d e8             mov    %r9d,-0x18(%rbp)
  400559:       8b 7d f4                mov    -0xc(%rbp),%edi
  40055c:       8b 75 f8                mov    -0x8(%rbp),%esi
  40055f:       8b 55 fc                mov    -0x4(%rbp),%edx
  400562:       8b 45 18                mov    0x18(%rbp),%eax # a8
  400565:       8b 4d e8                mov    -0x18(%rbp),%ecx
  400568:       89 4c 24 10             mov    %ecx,0x10(%rsp)
  40056c:       8b 4d ec                mov    -0x14(%rbp),%ecx
  40056f:       89 4c 24 08             mov    %ecx,0x8(%rsp)
  400573:       8b 4d f0                mov    -0x10(%rbp),%ecx
  400576:       89 0c 24                mov    %ecx,(%rsp)
  400579:       41 89 f9                mov    %edi,%r9d
  40057c:       41 89 f0                mov    %esi,%r8d
  40057f:       89 d1                   mov    %edx,%ecx
  400581:       8b 55 10                mov    0x10(%rbp),%edx # a7
  400584:       89 c6                   mov    %eax,%esi # a8
  400586:       bf 64 06 40 00          mov    $0x400664,%edi
  40058b:       b8 00 00 00 00          mov    $0x0,%eax
  400590:       e8 8b fe ff ff          callq  400420 <printf@plt>
  400595:       c9                      leaveq 
 
0000000000400597 <main>:
  400597:       55                      push   %rbp
  400598:       48 89 e5                mov    %rsp,%rbp
  40059b:       48 83 ec 10             sub    $0x10,%rsp
  40059f:       c7 44 24 08 08 00 00    movl   $0x8,0x8(%rsp) # 儲存引數 8 
  4005a6:       00 
  4005a7:       c7 04 24 07 00 00 00    movl   $0x7,(%rsp) # 儲存引數 7 
  4005ae:       41 b9 06 00 00 00       mov    $0x6,%r9d # 儲存引數 6 
  4005b4:       41 b8 05 00 00 00       mov    $0x5,%r8d # 儲存引數 5 
  4005ba:       b9 04 00 00 00          mov    $0x4,%ecx # 儲存引數 4 
  4005bf:       ba 03 00 00 00          mov    $0x3,%edx # 儲存引數 3 
  4005c4:       be 02 00 00 00          mov    $0x2,%esi # 儲存引數 2 
  4005c9:       bf 01 00 00 00          mov    $0x1,%edi # 儲存引數 1
  4005ce:       e8 6a ff ff ff          callq  40053d <echo>
  4005d3:       b8 00 00 00 00          mov    $0x0,%eax
  4005d8:       c9                      leaveq 
  4005d9:       c3                      retq   
  4005da:       66 0f 1f 44 00 00       nopw   0x0(%rax,%rax,1)

從上面的組合程式我們可以知道 1 - 6,這幾個引數確實是通過暫存器傳遞的,對應的暫存器就是上文當中我們提到不同的引數對應的暫存器。但是引數 7 和引數 8 是儲存在棧上的。根據上面的 main 函數的組合程式分析,他對應的棧幀的記憶體佈局如下所示:

我們在來分析一下 echo 函數當中 printf 函數引數的傳遞情況,第二個引數和第三個引數分別是 a8, a7,應該分別儲存到暫存器 rsi/esi, rdx/edx 當中,在上面的組合程式碼當中已經使用註釋的方式進行標註出來了,從下往上進行分析可以看到 a8 儲存在位置 0x18(%rbp),a7 儲存在 0x10(%rbp),這個地址正是 main 函數儲存 a7(當進入函數 echo 之後,a7,和 a8 的位置分別是 rsp + 0x10), a8(當進入函數 echo 之後,a7,和 a8 的位置分別是 rsp + 0x10 + 0x8) 的位置,具體可以結合上面的記憶體佈局圖進行分析。

dynamic 排程方式分析

我們使用下面的程式碼來分析一下動態排程的情況下整個程式的執行流程是怎麼樣的:

#pragma omp parallel for num_threads(t) schedule(dynamic, size)
for (i = lb; i <= ub; i++)
  body;

編譯器會將上面的程式編譯成下面的形式:

void subfunction (void *data)
{
  long _s0, _e0;
  while (GOMP_loop_dynamic_next (&_s0, &_e0))
  {
    long _e1 = _e0, i;
    for (i = _s0; i < _e1; i++)
      body;
  }
  // GOMP_loop_end_nowait 這個函數的主要作用就是釋放資料的記憶體空間 在後文當中不進行分析
  GOMP_loop_end_nowait ();
}
 
GOMP_parallel_loop_dynamic_start (subfunction, NULL, t, lb, ub+1, 1, size);
subfunction (NULL);
// 這個函數在前面的很多文章已經分析過 本文也不在進行分析
GOMP_parallel_end ();
void
GOMP_parallel_loop_dynamic_start (void (*fn) (void *), void *data,
				  unsigned num_threads, long start, long end,
				  long incr, long chunk_size)
{
  gomp_parallel_loop_start (fn, data, num_threads, start, end, incr,
			    GFS_DYNAMIC, chunk_size);
}
 
static void
gomp_parallel_loop_start (void (*fn) (void *), void *data,
			  unsigned num_threads, long start, long end,
			  long incr, enum gomp_schedule_type sched,
			  long chunk_size)
{
  struct gomp_team *team;
  // 解析具體建立多少個執行緒
  num_threads = gomp_resolve_num_threads (num_threads, 0);
  // 建立一個含有 num_threads 個執行緒的執行緒組
  team = gomp_new_team (num_threads);
  // 對執行緒組的資料進行初始化操作
  gomp_loop_init (&team->work_shares[0], start, end, incr, sched, chunk_size);
  // 啟動 num_threads 個執行緒執行函數 fn 
  gomp_team_start (fn, data, num_threads, team);
}
 
enum gomp_schedule_type
{
  GFS_RUNTIME, // runtime 排程方式
  GFS_STATIC,	 // static  排程方式
  GFS_DYNAMIC, // dynamic 排程方式
  GFS_GUIDED,	 // guided  排程方式
  GFS_AUTO     // auto    排程方式
};
 

在上面的程式當中 GOMP_parallel_loop_dynamic_start,有 7 個引數,我們接下來仔細解釋一下這七個引數的含義:

  • fn,函數指標也就是並行域被編譯之後的函數。
  • data,指向共用或者私有的資料,在並行域當中可能會使用外部的一些變數。
  • num_threads,並行域當中指定啟動執行緒的個數。
  • start,for 迴圈迭代的初始值,比如 for(int i = 0;

IT145.com E-mail:sddin#qq.com