OPENMP FOR CONSTRUCT GUIDED 排程方式實現原理和原始碼分析
前言
在本篇文章當中主要給大家介紹在 OpenMP 當中 guided 排程方式的實現原理。這個排程方式其實和 dynamic 排程方式非常相似的,從編譯器角度來說基本上是一樣的,在本篇文章當中就不介紹一些相關的必備知識了,如果不瞭解可以先看這篇文章 OpenMP For Construct dynamic 排程方式實現原理和原始碼分析 。
GUIDED 排程方式分析
我們使用下面的程式碼來分析一下 guided 排程的情況下整個程式的執行流程是怎麼樣的:
#pragma omp parallel for num_threads(t) schedule(guided, size)
for (i = lb; i <= ub; i++)
body;
編譯器會將上面的程式編譯成下面的形式:
void subfunction (void *data)
{
long _s0, _e0;
while (GOMP_loop_guided_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_guided_start (subfunction, NULL, t, lb, ub+1, 1, size);
subfunction (NULL);
// 這個函式在前面的很多文章已經分析過 本文也不在進行分析
GOMP_parallel_end ();
根據上面的程式碼可以知道,上面的程式碼當中最主要的兩個函式就是 GOMP_parallel_loop_guided_start 和 GOMP_loop_guided_next,現在我們來分析一下他們的原始碼:
- GOMP_parallel_loop_guided_start
void
GOMP_parallel_loop_guided_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_GUIDED, 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);
// 建立執行緒組
team = gomp_new_team (num_threads);
// 對共享資料進行初始化操作
gomp_loop_init (&team->work_shares[0], start, end, incr, sched, chunk_size);
// 啟動執行緒組執行函式 fn
gomp_team_start (fn, data, num_threads, team);
}
在上面的程式當中 GOMP_parallel_loop_guided_start,有 7 個引數,我們接下來仔細解釋一下這七個引數的含義:
- fn,函式指標也就是並行域被編譯之後的函式。
- data,指向共享或者私有的資料,在並行域當中可能會使用外部的一些變數。
- num_threads,並行域當中指定啟動執行緒的個數。
- start,for 迴圈迭代的初始值,比如 for(int i = 0; ? 這個 start 就是 0 。
- end,for 迴圈迭代的最終值,比如 for(int i = 0; i < 100; i++) 這個 end 就是 100 。
- incr,這個值一般都是 1 或者 -1,如果是 for 迴圈是從小到達迭代這個值就是 1,反之就是 -1,實際上這個值指的是 for 迴圈 i 大的增量。
- chunk_size,這個就是給一個執行緒劃分塊的時候一個塊的大小,比如 schedule(dynamic, 1),這個 chunk_size 就等於 1 。
事實上上面的程式碼和 GOMP_parallel_loop_dynamic_start 基本上一模一樣,函式引數也一致,唯一的區別就是排程方式的不同,上面的程式碼和前面的文章 OpenMP For Construct dynamic 排程方式實現原理和原始碼分析 基本一樣因此不再進行詳細的分析。
- GOMP_loop_guided_next,這是整個 guided 排程方式的核心程式碼(整個過程仍然使用 CAS 進行原子操作,保證併發安全)
static bool
gomp_loop_guided_next (long *istart, long *iend)
{
bool ret;
ret = gomp_iter_guided_next (istart, iend);
return ret;
}
bool
gomp_iter_guided_next (long *pstart, long *pend)
{
struct gomp_thread *thr = gomp_thread ();
struct gomp_work_share *ws = thr->ts.work_share;
struct gomp_team *team = thr->ts.team;
unsigned long nthreads = team ? team->nthreads : 1;
long start, end, nend, incr;
unsigned long chunk_size;
// 下一個分塊的起始位置
start = ws->next;
// 最終位置 不能夠超過這個位置
end = ws->end;
incr = ws->incr;
// chunk_size 是每個執行緒的分塊大小
chunk_size = ws->chunk_size;
while (1)
{
unsigned long n, q;
long tmp;
// 如果下一個分塊的起始位置等於最終位置 那就說明沒有需要繼續分塊的了 因此返回 false 表示沒有分塊需要執行了
if (start == end)
return false;
// 下面就是整個劃分的邏輯 大家可以吧 incr = 1 帶入 就能夠知道每次執行緒分得的資料就是當前剩下的資料處以執行緒的個數
n = (end - start) / incr;
q = (n + nthreads - 1) / nthreads;
if (q < chunk_size)
q = chunk_size;
if (__builtin_expect (q <= n, 1))
nend = start + q * incr;
else
nend = end;
// 進行比較並交換操作 比較 start 和 ws->next 的值,如果相等則將 ws->next 的值變為 nend 並且返回 ws->next 原來的值
tmp = __sync_val_compare_and_swap (&ws->next, start, nend);
if (__builtin_expect (tmp == start, 1))
break;
start = tmp;
}
*pstart = start;
*pend = nend;
return true;
}
從上面的整個分析過程來看,guided 排程方式之所以每個執行緒的分塊呈現遞減趨勢,是因為每次執行完一個 chunk size 之後,剩下的總的資料就少了,然後又除以執行緒數,因此每次得到的 chunk size 都是單調遞減的。
總結
在本篇文章當中主要介紹了 OpenMP 當中 guided 排程方式當中資料的劃分策略以及具體的實現程式碼, OpenMP 當中 for 迴圈的幾種排程策略的越程式碼是非常相似的,只有具體的劃分策略的 xxx_next 程式碼實現不同,因此整體來說是相對比較好閱讀的。guided 排程方式主要是用剩下的資料個數除以執行緒的個數就是執行緒所得到的 chunk size 的大小,然後更新剩下的資料個數再次除以執行緒的個數就是下一個執行緒所得到的 chunk size 大小,如此反覆直到劃分完成。
更多精彩內容合集可訪問專案:https://github.com/Chang-LeHung/CSCore
關注公眾號:一無是處的研究僧,瞭解更多計算機(Java、Python、計算機系統基礎、演算法與資料結構)知識。