OpenMP 環境變數使用總結

一無是處的研究僧發表於2023-01-19

OpenMP 環境變數使用總結

  • OMP_CANCELLATION,在 OpenMP 規範 4.5 當中規定了取消機制,我們可以使用這個環境變數去設定是否啟動取消機制,如果這個值等於 TRUE 那麼就是開啟執行緒取消機制,如果這個值等於 FALSE 那麼就是關閉取消機制。
#include <stdio.h>
#include <omp.h>

int main()
{

   int s = omp_get_cancellation();
   printf("%d\n", s);
#pragma omp parallel num_threads(8) default(none)
   {
      if (omp_get_thread_num() == 2)
      {
#pragma omp cancel parallel
      }

      printf("tid = %d\n", omp_get_thread_num());

   }
   return 0;
}

在上面的程式當中,如果我們啟動取消機制,那麼執行緒號等於 2 的執行緒就不會執行後面的 printf 語句。

➜  cmake-build-hun git:(master) ✗  export OMP_CANCELLATION=TRUE # 啟動取消機制
➜  cmake-build-hun git:(master) ✗ ./cancel 
1
tid = 0
tid = 4
tid = 1
tid = 3
tid = 5
tid = 6
tid = 7
  • OMP_DISPLAY_ENV,這個環境變數的作用就是程式在執行的時候首先會列印 OpenMP 相關的環境變數。如果這個環境變數值等於 TRUE 就會列印環境變數的值,如果是 FLASE 就不會列印。
➜  cmake-build-hun git:(master) ✗ export OMP_DISPLAY_ENV=TRUE   
➜  cmake-build-hun git:(master) ✗ ./critical 

OPENMP DISPLAY ENVIRONMENT BEGIN
  _OPENMP = '201511'
  OMP_DYNAMIC = 'FALSE'
  OMP_NESTED = 'FALSE'
  OMP_NUM_THREADS = '32'
  OMP_SCHEDULE = 'DYNAMIC'
  OMP_PROC_BIND = 'FALSE'
  OMP_PLACES = ''
  OMP_STACKSIZE = '0'
  OMP_WAIT_POLICY = 'PASSIVE'
  OMP_THREAD_LIMIT = '4294967295'
  OMP_MAX_ACTIVE_LEVELS = '2147483647'
  OMP_CANCELLATION = 'TRUE'
  OMP_DEFAULT_DEVICE = '0'
  OMP_MAX_TASK_PRIORITY = '0'
  OMP_DISPLAY_AFFINITY = 'FALSE'
  OMP_AFFINITY_FORMAT = 'level %L thread %i affinity %A'
OPENMP DISPLAY ENVIRONMENT END
data = 0
  • OMP_DYNAMIC,如果將這個環境變數設定為true,OpenMP實現可以調整用於執行並行區域的執行緒數,以最佳化系統資源的使用。與這個環境變數相關的一共有兩個函式:
void omp_set_dynamic(int);
int omp_get_dynamic(void);

omp_set_dynamic 使用這個函式表示是否設定動態調整執行緒的個數,如果傳入的引數不等於 0 表示開始,如果引數等於 0 就表示關閉動態調整。

我們現在來談一談 dynamic 動態調整執行緒個數以最佳化系統資源的使用是什麼意思,這個意思就是 OpenMP 建立的執行緒個數在同一個時刻不會超過你係統的處理器的個數,因為 OpenMP 常常用在資料密集型任務當中,這類任務對 CPU 的需求大,因此為了充分利用資源,只會建立處理器個數的執行緒個數。

下面我們使用一個例子來驗證上面所談到的內容。

#include <omp.h>
#include <stdio.h>

int main(int argc, char* argv[])
{
//   omp_set_dynamic(1);

#pragma omp parallel num_threads(33) default(none)
   {
      printf("tid = %d\n", omp_get_thread_num());
   }

   return 0;
}

上面的程式碼如果我們沒有設定 OMP_DYNAMIC=TRUE 或者沒有使用 omp_set_dynamic(1) 去啟動態調整的話,那麼上面的 printf 語句會被執行 33 次,但是如果你進行了設定,也就是啟動了動態調整執行緒的個數的話,那麼建立的執行緒個數就是 min(33, num_processors) ,後者是你的機器的處理器的個數,比如如果處理器的核的個數是 16 那麼就只會有 16 個執行緒執行並行域當中的程式碼。

  • OMP_NESTED,這個表示是否開啟並行域的巢狀模式,這個環境變數要麼是 TRUE 或者 FALSE ,如果這個環境變數的值為 TRUE 那麼能夠巢狀的最大的並行域的數量受到環境變數 OMP_MAX_ACTIVE_LEVELS 的限制,與這個環境變數相關的一個動態庫函式為 void omp_set_nested(int nested); ,表示是否開啟巢狀的並行域。
  • OMP_NUM_THREADS,這個表示設定並行域的執行緒個數,與這個環境變數相關的有 num_threads 這個子句和動態庫函式 void omp_set_num_threads(int num_threads);也是相關的。他們的優先順序為:num_threads > omp_set_num_threads > OMP_NUM_THREADS。這個環境變數的值必須是一個大於 0 的整數,關於他們的優先順序你可以認為離並行域越遠的就優先順序越低,反之越高。
  • OMP_STACKSIZE,這個環境變數的主要作用就是設定一個執行緒的棧空間的大小。
  • OMP_WAIT_POLICY,這個引數的主要作用就是控制當執行緒沒有拿到鎖的時候是自旋獲取鎖還是進入核心被掛起。這個引數主要有兩個值,active 或者 passive。
    • PASSIVE,等待的執行緒不消耗 CPU ,而是進入核心掛起。
    • ACTIVE,等待的執行緒消耗 CPU,一直自旋獲取鎖。

我們現在使用例子來驗證上面的規則:

#include <stdio.h>
#include <omp.h>

int main()
{
   omp_lock_t lock;
   omp_init_lock(&lock);
#pragma omp parallel num_threads(16) default(none) shared(lock)
   {
      omp_set_lock(&lock);
      while (1);
      omp_unset_lock(&lock);
   }
   return 0;
}

在上面的程式碼當中有一個並行域,並行域中執行緒的個數是 16,我們首先使用 ACTIVE 來看一下這個程式的負載,根據前面我們的描述那麼 16 個執行緒都會在自旋獲取鎖,這個過程將會一直使用 CPU,因此這個程式的負載 %CPU ,應該是接近 1600 % ,每個執行緒都是 100% 加起來就是 1600 % 。

➜  cmake-build-openmp export OMP_WAIT_POLICY=ACTIVE 
➜  cmake-build-openmp ./wait_policy                

我們使用 top 命令檢視一下這個程式的 CPU 使用率。

top - 17:27:14 up 263 days,  2:11,  2 users,  load average: 93.87, 87.59, 85.78
Tasks:  31 total,   2 running,  29 sleeping,   0 stopped,   0 zombie
%Cpu(s): 80.0 us,  0.7 sy,  0.0 ni, 19.2 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem : 13191648+total, 54673112 free, 15049648 used, 62193724 buff/cache
KiB Swap: 12499968+total, 11869649+free,  6303184 used. 11600438+avail Mem

   PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
112290 root      20   0  133868   1576   1452 R  1600  0.0  11:52.84 wait_policy

根據上面的輸出結果我們可以看到我們的預測是對的,所有的執行緒都活躍的在使用 CPU。

現在我們再來看一下如果我們使用 PASSIVE 的情況會是怎麼樣的?根據前面的描述如果執行緒沒有獲取到鎖那麼就會被掛起,因為只能夠有一個執行緒獲取到鎖,其餘 15 個執行緒都將被掛起,因此 CPU 的使用率應該是 100 % 左右,這個執行緒就是那個獲取到鎖的執行緒。

➜  cmake-build-openmp export OMP_WAIT_POLICY=PASSIVE
➜  cmake-build-openmp ./wait_policy 

我們再使用 top 命令檢視一下對應的輸出:

top - 17:27:53 up 263 days,  2:11,  2 users,  load average: 92.76, 88.10, 86.03
Tasks:  31 total,   2 running,  29 sleeping,   0 stopped,   0 zombie
%Cpu(s): 53.3 us,  0.8 sy,  0.0 ni, 45.9 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem : 13191648+total, 54675824 free, 15046932 used, 62193728 buff/cache
KiB Swap: 12499968+total, 11869649+free,  6303184 used. 11600710+avail Mem

   PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
112317 root      20   0  133868   1624   1496 R  99.3  0.0   0:04.58 wait_policy

從上面的輸出結果來看也是符合我們的預期,只有一個執行緒在不斷的使用 CPU。

  • GOMP_SPINCOUNT,這個環境變數的主要作用就是當 OMP_WAIT_POLICY 是 active 的時候,最多忙等待自旋多少次,如果自旋的次數超過這個值的話,那麼這個執行緒將會被掛起。

    當這個環境變數沒有定義:

    • OMP_WAIT_POLICY=PASSIVE,那麼自旋次數為 0 。
    • 如果 OMP_WAIT_POLICY 也是未定義的話,那麼這個自旋次數將會被設定成 300,000 。
    • OMP_WAIT_POLICY=ACTIVE,那麼自旋的次數是 300 億次。

    另外如果 OpenMP 的執行緒的個數大於可用的 CPU 的核心的個數的時候,1000 和 100 次就是 GOMP_SPINCOUNT 的值,對應OMP_WAIT_POLICY=ACTIVE 和 OMP_WAIT_POLICY 沒有定義。

  • OMP_MAX_TASK_PRIORITY,這個是設定 OpenMP 任務的優先順序的最大值,這個值應該是一個大於等於 0 的值,如果沒有定義,預設優先順序的值就是 0 。

  • OMP_MAX_ACTIVE_LEVELS,這個引數的主要作用是設定最大的巢狀的並行域的個數。

  • GOMP_CPU_AFFINITY,這個環境變數的作用就是將執行緒繫結到特定的 CPU 核心上。該變數應包含以空格分隔或逗號分隔的CPU列表。此列表可能包含不同型別的條目:任意順序的單個CPU編號、CPU範圍(M-N)或具有一定步長的範圍(M-N:S)。CPU編號從零開始。例如,GOMP_CPU_AFFINITY=“0 3 1-2 4-15:2”將分別將初始執行緒繫結到CPU 0,第二個繫結到CPU 3,第三個繫結到CPU1,第四個繫結到CPU 2,第五個繫結到CPU 4,第六個到第十個繫結到ccu 6、8、10、12和14,然後從列表的開頭開始重新分配。GOMP_CPU_AFFINITY=0將所有執行緒繫結到CPU 0。

我們現在來使用一個例子檢視環境變數的使用。我們的測試程式如下:

#include <stdio.h>
#include <omp.h>

int main()
{
   omp_lock_t lock;
   omp_init_lock(&lock);
#pragma omp parallel num_threads(4) default(none) shared(lock)
   {
      while (1);
   }
   return 0;
}

上面的程式就是開啟四個執行緒然後進行死迴圈。在我的測試環境中一共有 4 個 CPU 計算核心。我們現在執行上面的程式,對應的結果如下所示,下面的圖是使用命令 htop 得到的結果:

➜  tmp ./a.out
────────────────────────────────────────────────────────────────────────────────

    0[||||||||||||||||||||||||100.0%]   Tasks: 118, 212 thr; 4 running
    1[||||||||||||||||||||||||100.0%]   Load average: 2.62 0.86 0.29
    2[||||||||||||||||||||||||100.0%]   Uptime: 04:21:10
    3[||||||||||||||||||||||||100.0%]
  Mem[||||||||||||||||||||575M/3.82G]
  Swp[                      0K/3.82G]

    PID USER      PRI  NI  VIRT   RES   SHR S CPU%▽MEM%   TIME+  Command
  10750 lehung     20   0 27304   852   756 R 400.  0.0  2:30.53 ./a.out

從上面 htop 命令的輸出結果可以看到 0 - 3 四個核心都跑滿了,我們現在來看一下如果我們使用 GOMP_CPU_AFFINITY 環境變數使用執行緒繫結的方式 CPU 的負載將會是什麼樣!下面我們將所有的執行緒繫結到 0 1 兩個核心,那麼根據我們之前的分析 0 號核心上將會有第一個和第三個執行緒,1 號核心將會有第二個和第四個執行緒在上面執行。

➜  tmp export GOMP_CPU_AFFINITY="0 1"
➜  tmp ./a.out
────────────────────────────────────────────────────────────────────────────────

    0[||||||||||||||||||||||||100.0%]   Tasks: 118, 213 thr; 4 running
    1[||||||||||||||||||||||||100.0%]   Load average: 2.29 1.10 0.41
    2[|                         1.3%]   Uptime: 04:22:03
    3[|                         0.7%]
  Mem[||||||||||||||||||||576M/3.82G]
  Swp[                      0K/3.82G]

    PID USER      PRI  NI  VIRT   RES   SHR S CPU%▽MEM%   TIME+  Command
  10772 lehung     20   0 27304   840   744 R 200.  0.0  0:10.42 ./a.out

其實與上面的過程相關的兩個主要的系統呼叫就是:

int sched_setaffinity(pid_t pid, size_t cpusetsize,
                             const cpu_set_t *mask);
int sched_getaffinity(pid_t pid, size_t cpusetsize,
                             cpu_set_t *mask);

感興趣的同學可能檢視一下上面的兩個函式的手冊。

  • OMP_SCHEDULE,這個環境變數主要是用在 OpenMP 關於 for 迴圈的排程上的,他的規則為 OMP_SCHEDULE=type[,chunk],其中 type 的取值可以為 static, dynamic, guided, auto 。並且 chunk size 是可選的,而且他的值是一個正整數。如果這個環境變數沒有定義,預設的排程方式是 dynamic 並且 chunk size = 1 。

總結

在本篇文章當中主要給大家介紹了一些經常使用的 OpenMP 系統環境變數,設定環境變數有時候能夠更加方便的設定程式,同時有些環境變數對應一些 OpenMP 的動態庫函式。以上就是本篇文章的所有內容希望大家有所收穫!


更多精彩內容合集可訪問專案:https://github.com/Chang-LeHung/CSCore

關注公眾號:一無是處的研究僧,瞭解更多計算機(Java、Python、計算機系統基礎、演演算法與資料結構)知識。

相關文章