OpenMP?Parallel?Construct的實(shí)現(xiàn)原理詳解
Parallel 分析——編譯器角度
在本小節(jié)當(dāng)中我們將從編譯器的角度去分析該如何處理 parallel construct 。首先從詞法分析和語法分析的角度來說這對(duì)編譯器并不難,只需要加上一些處理規(guī)則,關(guān)鍵是編譯器將一個(gè) parallel construct 具體編譯成了什么?
下面是一個(gè)非常簡(jiǎn)單的 parallel construct。
#pragma?omp?parallel { ??body; }
編譯器在遇到上面的 parallel construct 之后會(huì)將代碼編譯成下面的樣子:
void?subfunction?(void?*data) { ??use?data; ??body; } setup?data; GOMP_parallel_start?(subfunction,?&data,?num_threads); subfunction?(&data); GOMP_parallel_end?();
首先 parallel construct 中的代碼塊會(huì)被編譯成一個(gè)函數(shù) sub function,當(dāng)然了函數(shù)名不一定是這個(gè),然后會(huì)在使用 #pragma omp parallel
的函數(shù)當(dāng)中將一個(gè) parallel construct 編譯成 OpenMP 動(dòng)態(tài)庫函數(shù)的調(diào)用,在上面的偽代碼當(dāng)中也指出了,具體會(huì)調(diào)用 OpenMP 的兩個(gè)庫函數(shù) GOMP_parallel_start 和 GOMP_parallel_end ,并且主線程也會(huì)調(diào)用函數(shù) subfunction ,我們?cè)诤竺娴奈恼庐?dāng)中在仔細(xì)分析這兩個(gè)動(dòng)態(tài)庫函數(shù)的源代碼。
深入剖析 Parallel 動(dòng)態(tài)庫函數(shù)參數(shù)傳遞
動(dòng)態(tài)庫函數(shù)分析
在本小節(jié)當(dāng)中,我們主要去分析一下在 OpenMP 當(dāng)中共享參數(shù)是如何傳遞的,以及介紹函數(shù) GOMP_parallel_start 的幾個(gè)參數(shù)的含義。
首先我們分析函數(shù) GOMP_parallel_start 的參數(shù)含義,這個(gè)函數(shù)的函數(shù)原型如下:
void?GOMP_parallel_start?(void?(*fn)(void?*),?void?*data,?unsigned?num_threads)
上面這個(gè)函數(shù)一共有三個(gè)參數(shù):
第一個(gè)參數(shù) fn 是一個(gè)函數(shù)指針,主要是用于指向上面編譯出來的 subfunction 這個(gè)函數(shù)的,因?yàn)樾枰鄠€(gè)線程同時(shí)執(zhí)行這個(gè)函數(shù),因此需要將這個(gè)函數(shù)傳遞過去,讓不同的線程執(zhí)行。
第二個(gè)參數(shù)是傳遞的數(shù)據(jù),我們?cè)诓⑿杏虍?dāng)中會(huì)使用到共享的或者私有的數(shù)據(jù),這個(gè)指針主要是用于傳遞數(shù)據(jù)的,我們?cè)诤竺鏁?huì)仔細(xì)分析這個(gè)參數(shù)的使用。
第三個(gè)參數(shù)是表示 num_threads 子句指定的線程個(gè)數(shù),如果不指定這個(gè)子句默認(rèn)的參數(shù)是 0 ,但是如果你使用了 IF 子句并且條件是 false 的話,那么這個(gè)參數(shù)的值就是 1 。
這個(gè)函數(shù)的主要作用是啟動(dòng)一個(gè)或者多個(gè)線程,并且執(zhí)行函數(shù) fn 。
void?GOMP_parallel_end?(void)
這個(gè)函數(shù)的主要作用是進(jìn)行線程的同步,因?yàn)橐粋€(gè) parallel 并行域需要等待所有的線程都執(zhí)行完成之后才繼續(xù)往后執(zhí)行。除此之外還需要釋放線程組的資源并行返回到之前的 omp_in_parallel() 表示的狀態(tài)。
參數(shù)傳遞分析
我們現(xiàn)在使用下面的代碼來具體分析參數(shù)傳遞過程:
#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; }
我們首先來分析一下上面的兩個(gè)變量 data 和 two 的是如何被傳遞的,我們首先用圖的方式進(jìn)行表示,然后分析一下匯編程序并且對(duì)圖進(jìn)行驗(yàn)證。
上面的代碼當(dāng)中兩個(gè)變量 data
和 two
在內(nèi)存當(dāng)中的布局結(jié)構(gòu)大致如下所示(假設(shè) data 的初始位置時(shí) 0x0):
那么在函數(shù) GOMP_parallel_start 當(dāng)中傳遞的參數(shù) data 就是 0x0 也就是指向 data 的內(nèi)存地址,如下圖所示:
那么根據(jù)上面參數(shù)傳遞的情況,我們就可以在 subfunction 當(dāng)中使用 *(int*)data 得到 data 的值,使用 *((int*) ((char*)data + 4)) 得到 two 的值,如果是 private 傳遞的話我們就可以先拷貝這個(gè)數(shù)據(jù)再使用,如果是 shared 的話,那么我們就可以直接使用指針就行啦。
上面的程序我們用 pthread 大致描述一下,則 pthread 對(duì)應(yīng)的代碼如下所示:
#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() { ??//?在主函數(shù)申請(qǐng)?8?個(gè)字節(jié)的棧空間 ??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; }
匯編程序分析
在本節(jié)當(dāng)中我們將仔細(xì)去分析上面的程序所產(chǎn)生的匯編程序,在本文當(dāng)中的匯編程序基礎(chǔ) x86_64 平臺(tái)。在分析匯編程序之前我們首先需要了解一下 x86函數(shù)的調(diào)用規(guī)約,具體來說就是在進(jìn)行函數(shù)調(diào)用的時(shí)候哪些寄存器保存函數(shù)參數(shù)以及是第幾個(gè)函數(shù)參數(shù)。具體的規(guī)則如下所示:
寄存器 | 含義 |
---|---|
rdi | 第一個(gè)參數(shù) |
rsi | 第二個(gè)參數(shù) |
rdx | 第三個(gè)參數(shù) |
rcx | 第四個(gè)參數(shù) |
r8 | 第五個(gè)參數(shù) |
r9 | 第六個(gè)參數(shù) |
我們現(xiàn)在仔細(xì)分析一下上面的程序的 main 函數(shù)的反匯編程序:
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
從上面的反匯編程序我們可以看到在主函數(shù)的匯編代碼當(dāng)中確實(shí)調(diào)用了函數(shù) GOMP_parallel_start 和 GOMP_parallel_end,并且 subfunction 為 main._omp_fn.0 ,它對(duì)應(yīng)的匯編程序如下所示:
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 詳細(xì)參數(shù)分析
void (*fn)(void *)
, 我們現(xiàn)在來看一下函數(shù) GOMP_parallel_start 的第一個(gè)參數(shù),根據(jù)我們前面談到的第一個(gè)參數(shù)應(yīng)該保存在 rdi 寄存器,我們現(xiàn)在分析一下在 main 函數(shù)的反匯編程序當(dāng)中在調(diào)用函數(shù) GOMP_parallel_start 之前 rdi 寄存器的值。我們可以看到在 main 函數(shù)位置為 4006f8 的地方的指令 mov $0x40073d,%edi
可以看到 rdi 寄存器的值為 0x40073d (edi 寄存器是 rdi 寄存器的低 32 位),我們可以看到 函數(shù) main._omp_fn.0 的起始地址就是 0x40073d ,因此我們就可以在函數(shù) GOMP_parallel_start 使用這個(gè)函數(shù)指針了,最終在啟動(dòng)的線程當(dāng)中調(diào)用這個(gè)函數(shù)。
void *data
,這是函數(shù) GOMP_parallel_start 的第二個(gè)參數(shù),根據(jù)前面的分析第二個(gè)參數(shù)保存在 rsi 寄存器當(dāng)中,我現(xiàn)在將 main 數(shù)當(dāng)中和 rsi 相關(guān)的指令選擇出來:
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
上面的匯編程序的??臻g以及在調(diào)用函數(shù)之前 GOMP_parallel_start 部分寄存器的指向如下所示:
最終在調(diào)用函數(shù) GOMP_parallel_start 之前 rsi 寄存器的指向如上圖所示,上圖當(dāng)中 rsi 的指向的內(nèi)存地址作為參數(shù)傳遞過去。根據(jù)上文談到的 subfunction 中的參數(shù)可以知道,在函數(shù) main._omp_fn.0 當(dāng)中的 rdi 寄存器(也就是第一個(gè)參數(shù) *data)的值就是上圖當(dāng)中 rsi 寄存器指向的內(nèi)存地址的值(事實(shí)上也就是 rsi 寄存器的值)。大家可以自行對(duì)照著函數(shù) main._omp_fn.0 的匯編程序?qū)?rdi 寄存器的使用就可以知道這其中的參數(shù)傳遞的過程了。
unsigned num_threads
,根據(jù)前文提到的保存第三個(gè)參數(shù)的寄存器是 rdx,在 main 函數(shù)的位置 4006fd 處,指令為 mov $0x4,%edx,這和我們自己寫的程序是一致的都是 4 (0x4)。
動(dòng)態(tài)庫函數(shù)源碼分析
GOMP_parallel_start 源碼分析
我們首先來看一下函數(shù) 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)); }
在這里我們對(duì)函數(shù) gomp_team_start 進(jìn)行分析,其他兩個(gè)函數(shù) gomp_resolve_num_threads 和 gomp_new_team 只簡(jiǎn)單進(jìn)行作用說明,太細(xì)致的源碼分析其實(shí)是沒有必要的,感興趣的同學(xué)自行分析即可,我們只需要了解整個(gè)執(zhí)行流程即可。
- gomp_resolve_num_threads,這個(gè)函數(shù)的主要作用是最終確定需要幾個(gè)線程去執(zhí)行任務(wù),因?yàn)槲覀兛赡懿]有使用 num_threads 子句,而且這個(gè)值和環(huán)境變量也有關(guān)系,因此需要對(duì)線程的個(gè)數(shù)進(jìn)行確定。
- gomp_new_team,這個(gè)函數(shù)的主要作用是創(chuàng)建包含 num_threads 個(gè)線程數(shù)據(jù)的線程組,并且對(duì)數(shù)據(jù)進(jìn)行初始化操作。
- gomp_team_start,這個(gè)函數(shù)的主要作用是啟動(dòng) num_threads 個(gè)線程去執(zhí)行函數(shù) fn ,這其中涉及一些細(xì)節(jié),比如說線程的親和性(affinity)設(shè)置。
由于 gomp_team_start 的源代碼太長(zhǎng)了,這里只是節(jié)選部分源程序進(jìn)行分析:
??/*?Launch?new?threads.??*/ ??for?(;?i?<?nthreads;?++i,?++start_data) ????{ ??????pthread_t?pt; ??????int?err; ??????start_data->fn?=?fn;?//?這行代碼就是將?subfunction?函數(shù)指針進(jìn)行保存最終在函數(shù)??gomp_thread_start?當(dāng)中進(jìn)行調(diào)用 ??????start_data->fn_data?=?data;?//?這里保存函數(shù)?subfunction?的函數(shù)參數(shù) ??????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?我們可以使用函數(shù)?omp_get_thread_num?得到這個(gè)值 ??????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; ???//?如果使用了線程的親和性那么還需要進(jìn)行親和性設(shè)置 ??????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)); ????}
上面的程序就是最終啟動(dòng)線程的源程序,可以看到這是一個(gè) for 循環(huán)并且啟動(dòng) nthreads 個(gè)線程,pthread_create
是真正創(chuàng)建了線程的代碼,并且讓線程執(zhí)行函數(shù) gomp_thread_start 可以看到線程不是直接執(zhí)行 subfunction 而是將這個(gè)函數(shù)指針保存到 start_data 當(dāng)中,并且在函數(shù) gomp_thread_start 真正去調(diào)用這個(gè)函數(shù),看到這里大家應(yīng)該明白了整個(gè) parallel construct 的整個(gè)流程了。
gomp_thread_start 的函數(shù)題也相對(duì)比較長(zhǎng),在這里我們選中其中的比較重要的幾行代碼,其余的代碼進(jìn)行省略。對(duì)比上面線程啟動(dòng)的 pthread_create 語句我們可以知道,下面的程序真正的調(diào)用了 subfunction,并且給這個(gè)函數(shù)傳遞了對(duì)應(yīng)的參數(shù)。
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 分析
這個(gè)函數(shù)的主要作用就是一個(gè)同步點(diǎn),保證所有的線程都執(zhí)行完成之后再繼續(xù)往后執(zhí)行,這一部分的源代碼比較雜,其核心原理就是使用路障 barrier 去實(shí)現(xiàn)的,這其中是 OpenMP 自己實(shí)現(xiàn)的一個(gè) barrier 而不是直接使用 pthread 當(dāng)中的 barrier ,這一部分的源程序就不進(jìn)行仔細(xì)分析了,感興趣的同學(xué)可以自行閱讀,可以參考 OpenMP 鎖實(shí)現(xiàn)原理 。
總結(jié)
在本篇文章當(dāng)中主要給大家介紹了 parallel construct 的實(shí)現(xiàn)原理,以及他的動(dòng)態(tài)庫函數(shù)的調(diào)用以及源代碼分析,大家只需要了解整個(gè)流程不太需要死扣細(xì)節(jié)(這并無很大的用處)只有當(dāng)我們自己需要去實(shí)現(xiàn) OpenMP 的時(shí)候需要去了解這些細(xì)節(jié),不然我們只需要了解整個(gè)動(dòng)態(tài)庫的設(shè)計(jì)原理即可!
以上就是OpenMP Parallel Construct的實(shí)現(xiàn)原理詳解的詳細(xì)內(nèi)容,更多關(guān)于OpenMP Parallel Construct的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C++數(shù)位DP復(fù)雜度統(tǒng)計(jì)數(shù)字問題示例詳解
這篇文章主要為大家介紹了利用C++數(shù)位DP的復(fù)雜度來統(tǒng)計(jì)數(shù)字問題的示例實(shí)現(xiàn)過程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升值加薪2021-11-11C++中小數(shù)點(diǎn)輸出格式(實(shí)例代碼)
下面小編就為大家?guī)硪黄狢++中小數(shù)點(diǎn)輸出格式(實(shí)例代碼)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-06-06C++哈希表之線性探測(cè)法實(shí)現(xiàn)詳解
線性探測(cè)法的優(yōu)點(diǎn):只要散列表未滿,總能找到一個(gè)不沖突的散列地址;缺點(diǎn):每個(gè)產(chǎn)生沖突的記錄被散列到離沖突最近的空地址上,從而又增加了更多的沖突機(jī)會(huì)2022-05-05C++使用鏈表實(shí)現(xiàn)圖書管理系統(tǒng)
這篇文章主要介紹了C++使用鏈表實(shí)現(xiàn)圖書管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03Matlab實(shí)現(xiàn)簡(jiǎn)易紀(jì)念碑谷游戲的示例代碼
《紀(jì)念碑谷》是USTWO公司開發(fā)制作的解謎類手機(jī)游戲,在游戲中,通過探索隱藏小路、發(fā)現(xiàn)視力錯(cuò)覺以及躲避神秘的烏鴉人來幫助沉默公主艾達(dá)走出紀(jì)念碑迷陣。本文將用Matlab編寫簡(jiǎn)易版的紀(jì)念碑谷游戲,感興趣的可以了解一下2022-03-03C++實(shí)現(xiàn)學(xué)生成績(jī)管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了C++實(shí)現(xiàn)學(xué)生成績(jī)管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-12-12如何利用C語言實(shí)現(xiàn)最簡(jiǎn)單的HTTP服務(wù)器詳解
這篇文章主要給大家介紹了關(guān)于如何利用C語言實(shí)現(xiàn)最簡(jiǎn)單的HTTP服務(wù)器的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用C語言具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11C語言 數(shù)據(jù)結(jié)構(gòu)與算法之字符串詳解
這篇文章將帶大家深入了解C語言數(shù)據(jù)結(jié)構(gòu)與算法中的字符串,文中主要是介紹了字符串的定義、字符串的比較以及一些串的抽象數(shù)據(jù)類型,感興趣的可以學(xué)習(xí)一下2022-01-01