最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

OpenMP?Parallel?Construct的實現(xiàn)原理詳解

 更新時間:2023年01月28日 09:17:54   作者:一無是處的研究僧  
在本篇文章當(dāng)中我們將主要分析?OpenMP?當(dāng)中的?parallel?construct?具體時如何實現(xiàn)的,以及這個?construct?調(diào)用了哪些運行時庫函數(shù),并且詳細(xì)分析這期間的參數(shù)傳遞,需要的可以參考一下

Parallel 分析——編譯器角度

在本小節(jié)當(dāng)中我們將從編譯器的角度去分析該如何處理 parallel construct 。首先從詞法分析和語法分析的角度來說這對編譯器并不難,只需要加上一些處理規(guī)則,關(guān)鍵是編譯器將一個 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 中的代碼塊會被編譯成一個函數(shù) sub function,當(dāng)然了函數(shù)名不一定是這個,然后會在使用 #pragma omp parallel 的函數(shù)當(dāng)中將一個 parallel construct 編譯成 OpenMP 動態(tài)庫函數(shù)的調(diào)用,在上面的偽代碼當(dāng)中也指出了,具體會調(diào)用 OpenMP 的兩個庫函數(shù) GOMP_parallel_start 和 GOMP_parallel_end ,并且主線程也會調(diào)用函數(shù) subfunction ,我們在后面的文章當(dāng)中在仔細(xì)分析這兩個動態(tài)庫函數(shù)的源代碼。

深入剖析 Parallel 動態(tài)庫函數(shù)參數(shù)傳遞

動態(tài)庫函數(shù)分析

在本小節(jié)當(dāng)中,我們主要去分析一下在 OpenMP 當(dāng)中共享參數(shù)是如何傳遞的,以及介紹函數(shù) GOMP_parallel_start 的幾個參數(shù)的含義。

首先我們分析函數(shù) GOMP_parallel_start 的參數(shù)含義,這個函數(shù)的函數(shù)原型如下:

void?GOMP_parallel_start?(void?(*fn)(void?*),?void?*data,?unsigned?num_threads)

上面這個函數(shù)一共有三個參數(shù):

第一個參數(shù) fn 是一個函數(shù)指針,主要是用于指向上面編譯出來的 subfunction 這個函數(shù)的,因為需要多個線程同時執(zhí)行這個函數(shù),因此需要將這個函數(shù)傳遞過去,讓不同的線程執(zhí)行。

第二個參數(shù)是傳遞的數(shù)據(jù),我們在并行域當(dāng)中會使用到共享的或者私有的數(shù)據(jù),這個指針主要是用于傳遞數(shù)據(jù)的,我們在后面會仔細(xì)分析這個參數(shù)的使用。

第三個參數(shù)是表示 num_threads 子句指定的線程個數(shù),如果不指定這個子句默認(rèn)的參數(shù)是 0 ,但是如果你使用了 IF 子句并且條件是 false 的話,那么這個參數(shù)的值就是 1 。

這個函數(shù)的主要作用是啟動一個或者多個線程,并且執(zhí)行函數(shù) fn 。

void?GOMP_parallel_end?(void)

這個函數(shù)的主要作用是進行線程的同步,因為一個 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;
}

我們首先來分析一下上面的兩個變量 data 和 two 的是如何被傳遞的,我們首先用圖的方式進行表示,然后分析一下匯編程序并且對圖進行驗證。

上面的代碼當(dāng)中兩個變量 data 和 two 在內(nèi)存當(dāng)中的布局結(jié)構(gòu)大致如下所示(假設(shè) data 的初始位置時 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 傳遞的話我們就可以先拷貝這個數(shù)據(jù)再使用,如果是 shared 的話,那么我們就可以直接使用指針就行啦。

上面的程序我們用 pthread 大致描述一下,則 pthread 對應(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ù)申請?8?個字節(jié)的??臻g
??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 平臺。在分析匯編程序之前我們首先需要了解一下 x86函數(shù)的調(diào)用規(guī)約,具體來說就是在進行函數(shù)調(diào)用的時候哪些寄存器保存函數(shù)參數(shù)以及是第幾個函數(shù)參數(shù)。具體的規(guī)則如下所示:

寄存器含義
rdi第一個參數(shù)
rsi第二個參數(shù)
rdx第三個參數(shù)
rcx第四個參數(shù)
r8第五個參數(shù)
r9第六個參數(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)中確實調(diào)用了函數(shù) GOMP_parallel_start 和 GOMP_parallel_end,并且 subfunction 為 main._omp_fn.0 ,它對應(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 的第一個參數(shù),根據(jù)我們前面談到的第一個參數(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 使用這個函數(shù)指針了,最終在啟動的線程當(dāng)中調(diào)用這個函數(shù)。

void *data,這是函數(shù) GOMP_parallel_start 的第二個參數(shù),根據(jù)前面的分析第二個參數(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 寄存器(也就是第一個參數(shù) *data)的值就是上圖當(dāng)中 rsi 寄存器指向的內(nèi)存地址的值(事實上也就是 rsi 寄存器的值)。大家可以自行對照著函數(shù) main._omp_fn.0  的匯編程序?qū)?rdi 寄存器的使用就可以知道這其中的參數(shù)傳遞的過程了。

unsigned num_threads,根據(jù)前文提到的保存第三個參數(shù)的寄存器是 rdx,在 main 函數(shù)的位置 4006fd 處,指令為 mov $0x4,%edx,這和我們自己寫的程序是一致的都是 4 (0x4)。

動態(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));
}

在這里我們對函數(shù) gomp_team_start 進行分析,其他兩個函數(shù) gomp_resolve_num_threads 和 gomp_new_team 只簡單進行作用說明,太細(xì)致的源碼分析其實是沒有必要的,感興趣的同學(xué)自行分析即可,我們只需要了解整個執(zhí)行流程即可。

  • gomp_resolve_num_threads,這個函數(shù)的主要作用是最終確定需要幾個線程去執(zhí)行任務(wù),因為我們可能并沒有使用 num_threads 子句,而且這個值和環(huán)境變量也有關(guān)系,因此需要對線程的個數(shù)進行確定。
  • gomp_new_team,這個函數(shù)的主要作用是創(chuàng)建包含 num_threads 個線程數(shù)據(jù)的線程組,并且對數(shù)據(jù)進行初始化操作。
  • gomp_team_start,這個函數(shù)的主要作用是啟動 num_threads 個線程去執(zhí)行函數(shù) fn ,這其中涉及一些細(xì)節(jié),比如說線程的親和性(affinity)設(shè)置。

由于 gomp_team_start 的源代碼太長了,這里只是節(jié)選部分源程序進行分析:

??/*?Launch?new?threads.??*/
??for?(;?i?<?nthreads;?++i,?++start_data)
????{
??????pthread_t?pt;
??????int?err;

??????start_data->fn?=?fn;?//?這行代碼就是將?subfunction?函數(shù)指針進行保存最終在函數(shù)??gomp_thread_start?當(dāng)中進行調(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?得到這個值
??????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;
???//?如果使用了線程的親和性那么還需要進行親和性設(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));
????}

上面的程序就是最終啟動線程的源程序,可以看到這是一個 for 循環(huán)并且啟動 nthreads 個線程,pthread_create 是真正創(chuàng)建了線程的代碼,并且讓線程執(zhí)行函數(shù) gomp_thread_start 可以看到線程不是直接執(zhí)行 subfunction 而是將這個函數(shù)指針保存到 start_data 當(dāng)中,并且在函數(shù) gomp_thread_start 真正去調(diào)用這個函數(shù),看到這里大家應(yīng)該明白了整個 parallel construct 的整個流程了。

gomp_thread_start 的函數(shù)題也相對比較長,在這里我們選中其中的比較重要的幾行代碼,其余的代碼進行省略。對比上面線程啟動的 pthread_create 語句我們可以知道,下面的程序真正的調(diào)用了 subfunction,并且給這個函數(shù)傳遞了對應(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 分析

這個函數(shù)的主要作用就是一個同步點,保證所有的線程都執(zhí)行完成之后再繼續(xù)往后執(zhí)行,這一部分的源代碼比較雜,其核心原理就是使用路障 barrier 去實現(xiàn)的,這其中是 OpenMP 自己實現(xiàn)的一個 barrier 而不是直接使用 pthread 當(dāng)中的 barrier ,這一部分的源程序就不進行仔細(xì)分析了,感興趣的同學(xué)可以自行閱讀,可以參考 OpenMP 鎖實現(xiàn)原理 。

總結(jié)

在本篇文章當(dāng)中主要給大家介紹了 parallel construct 的實現(xiàn)原理,以及他的動態(tài)庫函數(shù)的調(diào)用以及源代碼分析,大家只需要了解整個流程不太需要死扣細(xì)節(jié)(這并無很大的用處)只有當(dāng)我們自己需要去實現(xiàn) OpenMP 的時候需要去了解這些細(xì)節(jié),不然我們只需要了解整個動態(tài)庫的設(shè)計原理即可!

以上就是OpenMP Parallel Construct的實現(xiàn)原理詳解的詳細(xì)內(nèi)容,更多關(guān)于OpenMP Parallel Construct的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • 淺談QT打包的兩種方式

    淺談QT打包的兩種方式

    本文主要介紹了淺談QT打包的兩種方式,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-03-03
  • floyd算法實現(xiàn)思路及實例代碼

    floyd算法實現(xiàn)思路及實例代碼

    這篇文章主要介紹了floyd算法實現(xiàn)思路及實例代碼,有需要的朋友可以參考一下
    2014-01-01
  • C++聲明extern變量和extern函數(shù)的用法

    C++聲明extern變量和extern函數(shù)的用法

    extern關(guān)鍵字可以用來聲明變量和函數(shù)作為外部變量或者外部函數(shù)供其它文件使用,所以本文給大家介紹了C++聲明extern變量和extern函數(shù)的用法,文中有相關(guān)的代碼示例供大家參考,需要的朋友可以參考下
    2024-11-11
  • QT出現(xiàn)沒有MySQL驅(qū)動手動編譯詳細(xì)步驟

    QT出現(xiàn)沒有MySQL驅(qū)動手動編譯詳細(xì)步驟

    這篇文章主要給大家介紹了關(guān)于QT出現(xiàn)沒有MySQL驅(qū)動手動編譯詳細(xì)步驟的相關(guān)資料,文中通過圖文介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用QT具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2023-04-04
  • C?語言的弱符號與弱引用你了解嗎

    C?語言的弱符號與弱引用你了解嗎

    這篇文章主要為大家詳細(xì)介紹了C?語言弱符號與弱引用,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助
    2022-03-03
  • C++數(shù)據(jù)結(jié)構(gòu)之并查集詳解

    C++數(shù)據(jù)結(jié)構(gòu)之并查集詳解

    這篇文章主要介紹了C++數(shù)據(jù)結(jié)構(gòu)之并查集詳解,并查集是一種樹型的數(shù)據(jù)結(jié)構(gòu),用于處理一些不相交集合的合并及查詢問題,并查集的思想是用一個數(shù)組表示了整片森林,需要的朋友可以參考下
    2023-08-08
  • C語言動態(tài)內(nèi)存分配和內(nèi)存操作函數(shù)使用詳解

    C語言動態(tài)內(nèi)存分配和內(nèi)存操作函數(shù)使用詳解

    但是在實際的編程中,往往會發(fā)生這種情況,即所需的內(nèi)存空間取決于實際輸入的數(shù)據(jù),而無法預(yù)先確定 。為了解決上述問題,C語言提供了一些內(nèi)存管理函數(shù),這些內(nèi)存管理函數(shù)可以按需要動態(tài)的分配內(nèi)存空間,也可把不再使用的空間回收再次利用
    2022-12-12
  • C++ 動態(tài)內(nèi)存分配詳解(new/new[]和delete/delete[])

    C++ 動態(tài)內(nèi)存分配詳解(new/new[]和delete/delete[])

    這篇文章主要介紹了C++ 動態(tài)內(nèi)存分配詳解(new/new[]和delete/delete[]),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2021-05-05
  • C++中spdlog的簡單使用示例

    C++中spdlog的簡單使用示例

    spdlog是一個開源、跨平臺、無依賴、只有頭文件的C++11日志庫,所以這篇文章主要來和大家介紹一下一個簡單的spdlog使用示例,感興趣的小伙伴可以了解一下
    2023-08-08
  • c++ 面向?qū)ο笤O(shè)計五大原則

    c++ 面向?qū)ο笤O(shè)計五大原則

    這篇文章主要介紹了c++ 面向?qū)ο笤O(shè)計五大原則,幫助大家更好的理解和學(xué)習(xí)c++面向?qū)ο笤O(shè)計,感興趣的朋友可以了解下
    2020-08-08

最新評論

阜新市| 门头沟区| 铁力市| 嘉兴市| 怀安县| 青铜峡市| 达日县| 英德市| 嘉兴市| 宣威市| 田东县| 福安市| 泾阳县| 东阿县| 宜兰市| 洪雅县| 乡宁县| 禹城市| 霍林郭勒市| 合川市| 西贡区| 怀宁县| 永州市| 沿河| 东阳市| 噶尔县| 抚州市| 赫章县| 阿克陶县| 台江县| 正宁县| 武安市| 林西县| 建阳市| 万载县| 利川市| 突泉县| 肇州县| 庆元县| 河源市| 兴宁市|