OpenMP 入門教程


前兩天(其實是幾個月以前了)看到了代碼中有 #pragma omp parallel for 一段,感覺好像是 OpenMP,以前看到並行化的東西都是直接躲開,既然躲不開了,不妨研究一下:

OpenMP 是 Open MultiProcessing 的縮寫。OpenMP 並不是一個簡單的函數庫,而是一個諸多編譯器支持的框架,或者說是協議吧,總之,不需要任何配置,你就可以在 Visual Studio 或者 gcc 中使用它了。

我們就分三部分來介紹吧,因為我看的那個英文教程就是分了三部分(哈哈) . 以下翻譯自英特爾的文檔

Hello World

把下面的代碼保存為 omp.cc

#include <iostream>
#include <omp.h>

int main()
{
#pragma omp parallel for
    for (char i = 'a'; i <= 'z'; i++)
        std::cout << i << std::endl;

    return 0;
}

然后 g++ omp.cc -fopenmp就可以了

入門

循環的並行化

OpenMP的設計們希望提供一種簡單的方式讓程序員不需要懂得創建和銷毀線程就能寫出多線程化程序。為此他們設計了一些pragma,指令和函數來讓編譯器能夠在合適的地方插入線程大多數的循環只需要在for之前插入一個pragma就可以實現並行化。而且,通過把這些惱人的細節都丟給編譯器,你可以花費更多的時間來決定哪里需要多線程和優化數據結構

下面個這個例子把32位的RGB顏色轉換成8位的灰度數據,你只需要在for之前加上一句pragma就可以實現並行化了

#pragma omp parallel for
for (int i = 0; i < pixelCount; i++) {
    grayBitmap[i] = (uint8_t)(rgbBitmap[i].r * 0.229 +
                              rgbBitmap[i].g * 0.587 +
                              rgbBitmap[i].b * 0.114);
}

神奇吧,首先,這個例子使用了“work sharing”,當“work sharing”被用在for循環的時候,每個循環都被分配到了不同的線程,並且保證只執行一次。OpenMP決定了多少線程需要被打開,銷毀和創建,你需要做的就是告訴OpenMP哪里需要被線程化。

OpenMP 對可以多線程化的循環有如下五個要求:

  1. 循環的變量變量(就是i)必須是有符號整形,其他的都不行。
  2. 循環的比較條件必須是< <= > >=中的一種
  3. 循環的增量部分必須是增減一個不變的值(即每次循環是不變的)。
  4. 如果比較符號是< <=,那每次循環i應該增加,反之應該減小
  5. 循環必須是沒有奇奇怪怪的東西,不能從內部循環跳到外部循環,goto和break只能在循環內部跳轉,異常必須在循環內部被捕獲。

如果你的循環不符合這些條件,那就只好改寫了

檢測是否支持 OpenMP

#ifndef _OPENMP
    fprintf(stderr, "OpenMP not supported");
#endif

避免數據依賴和競爭

當一個循環滿足以上五個條件時,依然可能因為數據依賴而不能夠合理的並行化。當兩個不同的迭代之間的數據存在依賴關系時,就會發生這種情況。

// 假設數組已經初始化為1
#pragma omp parallel for
for (int i = 2; i < 10; i++) {
    factorial[i] = i * factorial[i-1];
}

編譯器會把這個循環多線程化,但是並不能實現我們想要的加速效果,得出的數組含有錯誤的結構。因為每次迭代都依賴於另一個不同的迭代,這被稱之為競態條件。要解決這個問題只能夠重寫循環或者選擇不同的算法。

競態條件很難被檢測到,因為也有可能恰好程序是按你想要的順序執行的。

管理公有和私有數據

基本上每個循環都會讀寫數據,確定那個數據時線程之間共有的,那些數據時線程私有的就是程序員的責任了。當數據被設置為公有的時候,所有的線程訪問的都是相同的內存地址,當數據被設為私有的時候,每個線程都有自己的一份拷貝。默認情況下,除了循環變量以外,所有數據都被設定為公有的。可以通過以下兩種方法把變量設置為私有的:

  1. 在循環內部聲明變量,注意不要是static的
  2. 通過OpenMP指令聲明私有變量
// 下面這個例子是錯誤的
int temp; // 在循環之外聲明
#pragma omp parallel for
for (int i = 0; i < 100; i++) {
    temp = array[i];
    array[i] = doSomething(temp);
}

可以通過以下兩種方法改正

// 1. 在循環內部聲明變量
#pragma omp parallel for
for (int i = 0; i < 100; i++) {
    int temp = array[i];
    array[i] = doSomething(temp);
}
// 2. 通過OpenMP指令說明私有變量
int temp;
#pragma omp parallel for private(temp)
for (int i = 0; i < 100; i++) {
    temp = array[i];
    array[i] = doSomething(temp);
}

Reductions

一種常見的循環就是累加變量,對此,OpenMP 有專門的語句

例如下面的程序:

int sum = 0;
for (int i = 0; i < 100; i++) {
    sum += array[i]; // sum需要私有才能實現並行化,但是又必須是公有的才能產生正確結果
}

上面的這個程序里,sum公有或者私有都不對,為了解決這個問題,OpenMP 提供了reduction語句;

int sum = 0;
#pragma omp parallel for reduction(+:sum)
for (int i = 0; i < 100; i++) {
    sum += array[i];
}

內部實現中,OpenMP 為每個線程提供了私有的sum變量,當線程退出時,OpenMP 再把每個線程的部分和加在一起得到最終結果。

當然,OpenMP 不止能做累加,凡是累計運算都是可以的,如下表:

操作 私有臨時變量初值
+、- 0
* 1
& ~0
| 0
^ 0
&& 1(true)
|| 0(false

循環調度

負載均衡是多線程程序中對性能影響最大的因素了,只有實現了負載均衡才能保證所有的核心都是忙的,而不會出現空閑時間。如果沒有負載均衡, 有一些線程會遠遠早於其他線程結束, 導致處理器空閑浪費優化的可能.

在循環中,經常會由於每次迭代的相差時間較大和破壞負載平衡。通常可以通過檢查源碼來發現循環的變動可能. 大多數情況下每次迭代可能會發現大概一致的時間,當這個條件不能滿足的時候,你可能能找到一個花費了大概一致時間的子集。例如, 有時候所有偶數循環花費了和所有奇數循環一樣的時間, 有時候可能前一半循環和后一半循環花費了相似的時間. 另一方面, 有時候你可能找不到花費相同時間的一組循環. 不論如何, 你應該把這些信息提供給 OpenMP, 這樣才能讓 OpenMP 有更好的機會去優化循環.

默認情況下,OpenMP認為所有的循環迭代運行的時間都是一樣的,這就導致了OpenMP會把不同的迭代等分到不同的核心上,並且讓他們分布的盡可能減小內存訪問沖突,這樣做是因為循環一般會線性地訪問內存, 所以把循環按照前一半后一半的方法分配可以最大程度的減少沖突. 然而對內存訪問來說這可能是最好的方法, 但是對於負載均衡可能並不是最好的方法, 而且反過來最好的負載均衡可能也會破壞內存訪問. 因此必須折衷考慮.

OpenMP 負載均衡使用下面的語法

#pragma omp parallel for schedule(kind [, chunk size])

其中kind可以是下面的這些類型, 而 chunk size 則必須是循環不變的正整數

例子

#pragma omp parallel for
for (int i = 0; i  < numElements; i++) {
    array[i] = initValue;
    initValue++;
}

顯然這個循環里就有了競態條件, 每個循環都依賴於 initValue 這個變量, 我們需要去掉它.

#pragma omp parallel for
for (int i = 0; i < numElements; i++) {
    array[i] = initValue + i;
}

這樣就可以了, 因為現在我們沒有讓 initValue 去被依賴

所以, 對於一個循環來說, 應該盡可能地把 loop-variant 變量建立在 i 上.

待續...


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM