2020-10-19

2020-10-20 11:00:31

OpenMP 使用介紹

  1. OpenMP 基本概念
    Open Multi-Processing的縮寫,是一個應用程式介面(API),可用於顯式指導多執行緒、共用記憶體的並行性。
    在專案程式已經完成好的情況下不需要大幅度的修改原始碼,只需要加上專用的pragma來指明自己的意圖,由此編譯器可以自動將程式進行並行化,並在必要之處加入同步互斥以及通訊。當選擇忽略這些pragma,或者編譯器不支援OpenMp時,程式又可退化為通常的程式(一般為序列),程式碼仍然可以正常運作,只是不能利用多執行緒來加速程式執行。OpenMP提供的這種對於並行描述的高層抽象降低了並行程式設計的難度和複雜度,這樣程式設計師可以把更多的精力投入到並行演演算法本身,而非其具體實現細節。對基於資料分集的多執行緒程式設計,OpenMP是一個很好的選擇。
    OpenMP支援的語言套件括C/C++、Fortran;而支援OpenMP的編譯器VS、gcc、clang等都行。可移植性也很好:Unix/Linux和Windows

  2. OpenMP程式設計模型
      記憶體共用模型:OpenMP是專為多處理器/核,共用記憶體機器所設計的。底層架構可以是UMA和NUMA。即(Uniform Memory Access和Non-Uniform Memory Access)

2.1基於執行緒的並行性
• OpenMP僅通過執行緒來完成並行
• 一個執行緒的執行是可由作業系統呼叫的最小處理單
• 執行緒們存在於單個程序的資源中,沒有了這個程序,執行緒也不存在了
• 通常,執行緒數與機器的處理器/核數相匹配,然而,實際使用取決與應用程式
2.2明確的並行
• OpenMP是一種顯式(非自動)程式設計模型,為程式設計師提供對並行化的完全控制
• 一方面,並行化可像執行序列程式和插入編譯指令那樣簡單
• 另一方面,像插入子程式來設定多級並行、鎖、甚至巢狀鎖一樣複雜
2.3 Fork-Join模型

• OpenMP就是採用Fork-Join模型
• 所有的OpenML程式都以一個單個程序——master thread開始,master threads按順序執行知道遇到第一個並行區域
• Fork:主執行緒創造一個並行執行緒組
• Join:當執行緒組完成並行區域的語句時,它們同步、終止,僅留下主執行緒
2.4 資料範圍
• 由於OpenMP時是共用記憶體模型,預設情況下,在共用區域的大部分資料是被共用的
• 並行區域中的所有執行緒可以同時存取這個共用的資料
• 如果不需要預設的共用作用域,OpenMP為程式設計師提供一種「顯示」指定資料作用域的方法
2.5巢狀並行
• API提供在其它並行區域放置並行區域
• 實際實現也可能不支援
2.6動態執行緒
• API為執行環境提供動態的改變用於執行並行區域的執行緒數
• 實際實現也可能不支援

3.openmp使用
  需要使用openmp就需要引入omp.h庫檔案。然後在編譯時新增引數 -fopenmp即可。 在具體需要進行並行運算的部分,使用 #pragma omp 指令[子句] 來告訴編譯器如何並行執行對應的語句。 常用的指令如下:
parallel:用在一個結構塊之前,表示這段程式碼將被多個執行緒並行執行;
for:用於for迴圈語句之前,表示將回圈計算任務分配到多個執行緒中並行執行,以實現任務分擔,必須由程式設計人員自己保證每次迴圈之間無資料相關性;parallel for:parallel和for指令的結合,也是用在for迴圈語句之前,表示for迴圈體的程式碼將被多個執行緒並行執行,它同時具有並行域的產生和任務分擔兩個功能;
sections:用在可被並行執行的程式碼段之前,用於實現多個結構塊語句的任務分擔,可並行執行的程式碼段各自用section指令標出(注意區分sections和section);
parallel sections:parallel和sections兩個語句的結合,類似於parallel for;
single:用在並行域內,表示一段只被單個執行緒執行的程式碼;
critical:用在一段程式碼臨界區之前,保證每次只有一個OpenMP執行緒進入;
flush:保證各個OpenMP執行緒的資料影像的一致性;
barrier:用於並行域內程式碼的執行緒同步,執行緒執行到barrier時要停下等待,直到所有執行緒都執行到barrier時才繼續往下執行;
atomic:用於指定一個資料操作需要原子性地完成;
master:用於指定一段程式碼由主執行緒執行;
threadprivate:用於指定一個或多個變數是執行緒專用,後面會解釋執行緒專有和私有的區別。
  常用的子句如下:
private:指定一個或多個變數在每個執行緒中都有它自己的私有副本;
firstprivate:指定一個或多個變數在每個執行緒都有它自己的私有副本,並且私有變數要在進入並行域或任務分擔域時,繼承主執行緒中的同名變數的值作為初值;
lastprivate:是用來指定將執行緒中的一個或多個私有變數的值在並行處理結束後複製到主執行緒中的同名變數中,負責拷貝的執行緒是for或sections任務分擔中的最後一個執行緒;
reduction:用來指定一個或多個變數是私有的,並且在並行處理結束後這些變數要執行指定的歸約運算,並將結果返回給主執行緒同名變數;
nowait:指出並行執行緒可以忽略其他制導指令暗含的路障同步;
num_threads:指定並行域內的執行緒的數目;
schedule:指定for任務分擔中的任務分配排程型別;
shared:指定一個或多個變數為多個執行緒間的共用變數;
ordered:用來指定for任務分擔域內指定程式碼段需要按照序列迴圈次序執行;
copyprivate:配合single指令,將指定執行緒的專有變數廣播到並行域內其他執行緒的同名變數中;
copyin:用來指定一個threadprivate型別的變數需要用主執行緒同名變數進行初始化;
default:用來指定並行域內的變數的使用方式,預設是shared  
  另外,openmp還提供了一些列的api函數來獲取並行執行緒的狀態或控制並行執行緒的行為,常用的OpenMP API函數以及說明:
omp_in_parallel - 判斷當前是否在並行域中。
omp_get_thread_num - 獲取執行緒號
omp_set_num_threads - 設定並行域中執行緒格式
omp_get_num_threads - 返回並行域中執行緒數
omp_get_dynamic - 判斷是否支援動態改變執行緒數目
omp_get_max_threads - 獲取並行域中可用的最大的並行執行緒數目
omp_get_num_procs - 返回系統中處理器的個數  
  
環境變數
OpenMP中定義一些環境變數,可以通過這些環境變數控制OpenMP程式的行為,常用的環境變數:
OMP_SCHEDULE:用於for迴圈並行化後的排程,它的值就是迴圈排程的型別;
OMP_NUM_THREADS:用於設定並行域中的執行緒數;
OMP_DYNAMIC:通過設定變數值,來確定是否允許動態設定並行域內的執行緒數;
OMP_NESTED:指出是否可以並行巢狀。
4.openmp 範例1

#include <iostream>
#include <cstdlib>
#include <stdio.h>
#include <time.h>

using namespace std;

const int NumChunks       = 8;
const int ChunkSize       = 1024*16;
const int NumElements     = NumChunks*ChunkSize;

#define RAND_MAX_A 64
#define RAND_MAX_B 256
#define EPISILON  0.00001

float srcA  [NumElements];
float srcB  [NumElements];
float dst   [NumElements];
float Golden[NumElements];

void vadd_openmp(float *a, float *b, float *c, int size)
{
    #pragma omp target map(to:a[0:size],b[0:size]) map(from:c[0:size])   
    {
        int i;
        #pragma omp parallel for
        for (i = 0; i < size; i++)
            c[i] = a[i] + b[i];

    }

}

#pragma omp declare target
void compute(float *a, float *b, float *c, int si, int size);
#pragma omp end declare target

void vadd_openmp_t(float *a, float *b, float *c, int NumChunks, int ChunkSize)
{
    int NumElements = NumChunks*ChunkSize;
#pragma omp target map(to:a[0:NumElements],b[0:NumElements],NumChunks,ChunkSize) \
                   map(from:c[0:NumElements])
    {
        int i;
        #pragma omp parallel
        {
            #pragma omp master
            for (i = 0; i < NumChunks; i++)
            {
                int start_index = i * ChunkSize;
                #pragma omp task firstprivate (start_index, ChunkSize)
                compute(a, b, c, start_index, ChunkSize);
            }    
        }
    }
}

void compute(float *a, float *b, float *c, int si, int size)
{
    int i;
    for (i = si; i < si+size; i++)
        c[i] = a[i] + b[i];
}

int main()
{
    int    num_errors = 0;
    const int    print_nerrors = 12;
    
    /* ---------------------------------------------------------------- */
    /*  Initialized input arrays with random test data.                 */
    /* ---------------------------------------------------------------- */
    /* Initialize Random Number Seed */
    srand(time(NULL));
    
    for (int i=0; i < NumElements; ++i) 
    {
        srcA[i] = (rand() % RAND_MAX_A + 1) * 1.0; 
        srcB[i] = (rand() % RAND_MAX_B + 1) * 1.0;
        dst[i] = 0;
        Golden[i]   =   srcA[i] + srcB[i];
    }   

    /* ---------------------------------------------------------------- */
    /* Call vadd_openmp target code                                     */
    /* ---------------------------------------------------------------- */
    vadd_openmp(srcA,srcB,dst,NumElements);
    
    /* ---------------------------------------------------------------- */
    /* Perform error checking                                           */
    /* ---------------------------------------------------------------- */
    for (int i=0; i < NumElements; ++i)
    {
        if (Golden[i] - dst[i] < -EPISILON || Golden[i] - dst[i] > EPISILON) 
        { 
            if((num_errors += 1) < print_nerrors)
                printf("Error %d: %f <==> %f\n", i, Golden[i], dst[i]);
        }
    }
    if (num_errors > 0)  cout << "FAIL with " << num_errors << " errors!\n";
    else                  cout << "PASS!" << endl; 

    /* ---------------------------------------------------------------- */
    /* Call vadd_openmp target code                                     */
    /* ---------------------------------------------------------------- */
    vadd_openmp_t(srcA,srcB,dst,NumChunks, ChunkSize);
    
    /* ---------------------------------------------------------------- */
    /* Perform error checking                                           */
    /* ---------------------------------------------------------------- */
    for (int i=0; i < NumElements; ++i)
    {
        if (Golden[i] - dst[i] < -EPISILON || Golden[i] - dst[i] > EPISILON) 
        { 
            if((num_errors += 1) < print_nerrors)
                printf("Error %d: %f <==> %f\n", i, Golden[i], dst[i]);
        }
    }
    if (num_errors > 0)  cout << "FAIL with " << num_errors << " errors!\n";
    else                  cout << "PASS!" << endl; 

    return 0;
}

5.openmp 範例2

#include <iostream>
#include <cstdlib>
#include <stdio.h>
#include <time.h>

using namespace std;

const int NumElements     = 8*1024;

#define RAND_MAX_A 64
#define RAND_MAX_B 256
#define EPISILON  0.00001

float srcA  [NumElements];
float srcB  [NumElements];
float dst   [NumElements];
float Golden[NumElements];

void vadd_sub_section(float *a, float *b, float *c, int size)
{
    int start = 0;
    #pragma omp target data map(to:a[start:size], b[start:size]) map(tofrom:c[start:size])
    {
        /* At this point a,b have been copied to the device and space for c has also 
         * been allocated on the device */
        int i;
        int chunk_size = size/2;
        /* Operate on half of Array's a and b to generate half of c 
         * At this point, sub-sections are created out of the parent array sections a,b,c */
        #pragma omp target map(to:a[start:chunk_size], b[start:chunk_size]) \
                           map(tofrom:c[start:chunk_size])
        {
            #pragma omp parallel for private(i)
            for (i = start; i < start+chunk_size; i++)
                c[i] = a[i] + b[i];
        }
        
        /* Operate on the other half, simply by incrementing the start offset */
        start = chunk_size;
        #pragma omp target map(to:a[start:chunk_size], b[start:chunk_size]) \
                           map(tofrom:c[start:chunk_size])
        {
            #pragma omp parallel for private(i)
            for (i = start; i < start+chunk_size; i++)
                c[i] = a[i] + b[i];
        }
    }
}

int main()
{
    int    num_errors = 0;
    const int    print_nerrors = 12;
    
    /* ---------------------------------------------------------------- */
    /*  Initialized input arrays with random test data.                 */
    /* ---------------------------------------------------------------- */
    /* Initialize Random Number Seed */
    srand(time(NULL));
    
    for (int i=0; i < NumElements; ++i) 
    {
        srcA[i] = (rand() % RAND_MAX_A + 1) * 1.0; 
        srcB[i] = (rand() % RAND_MAX_B + 1) * 1.0;
        dst[i] = 0;
        Golden[i]   =   srcA[i] + srcB[i];
    }   

    /* ---------------------------------------------------------------- */
    /* Call vadd_sub_section                                            */
    /* ---------------------------------------------------------------- */
    vadd_sub_section(srcA,srcB,dst,NumElements);
    
    /* ---------------------------------------------------------------- */
    /* Perform error checking                                           */
    /* ---------------------------------------------------------------- */
    for (int i=0; i < NumElements; ++i)
    {
        if (Golden[i] - dst[i] < -EPISILON || Golden[i] - dst[i] > EPISILON) 
        { 
            if((num_errors += 1) < print_nerrors)
                printf("Error %d: %f <==> %f\n", i, Golden[i], dst[i]);
        }
    }
    if (num_errors > 0)  cout << "FAIL with " << num_errors << " errors!\n";
    else                  cout << "PASS!" << endl; 

    return 0;
}