前言
記憶體架構分成兩種共享式記憶體與分散式記憶體
共享式記憶體:記憶體是由多個CPU來共同存取的,所以通常CPU和記憶體會放在同一台電腦裡,優點是可以很快存取記憶體,缺點是擴充性不佳。
分散式記憶體:利用網路串連多台機器,優點是可以CPU就可以很快存取到記憶體,缺點是機器間的資料交換不易。
今天要介紹的OpenMp屬於利用共享式記憶體來對程式平行化處理,而MPI是利用分散式記憶體來對程式平行化處理。
環境架構
- CPU : Intel i5–6300HQ
- Main Memory : 8G
- Disk:128G SSD + 1T HDD
- OS : CentOS 8
What is OpenMp?
OpenMp(Open Multi-Processing)是一種利用thread進行平行化處理,進而加快程式處理的速度的函式庫,可跨平台使用。
程式語言:C,C++,Fortran
OpenMp架構
OpenMp會在進入parallel region將master thread複製好幾份放到記憶體內同時執行(從parallel region開始的地方執行),最後離開parallel region的時候會等待所有thread執行完畢後再繼續執行master thread的程式
OpenMp教學
首先設定預設的thread數量,當程式碼中沒有指定thread數量時則會使用預設或是以Logical CPU當作預設值,這邊先預設thread數量為2
$ export OMP_NUM_THREADS=2
OpenMp使用語法 : #pragma omp <directive> [clause[[,] clause] …]
OpenMp基本Function
- omp_get_thread_num() 取得目前thread的id
- omp_set_num_threads(n) 在程式中設定thread的數量
- omp_get _num_threads()取得使用中thread的數量
- omp_set_schedule()設定schedule的方法
Example1:利用parallel進行程式的平行化
#include<omp.h>
#include<stdio.h>int main(int argc,char* argv[]){
#pragma omp parallel
{
printf("Thread %d Hello World \n",omp_get_thread_num());
}
}
編譯與執行:-fopenmp 用來載入libgomp這個動態函式庫
$ gcc -fopenmp example.c
$ ./a.out
Output:因為設定兩個thread所以出現0和1,thread 0 為master thread
Example2:for迴圈平行化
#include<omp.h>
#include<stdio.h>int main(){#pragma omp parallel
{
#pragma omp for
for(int i=0;i<5;i++)
{
printf("thread %d : loop %d\n",omp_get_thread_num(),i);
}
}
return 0;
}
Output:因為是平行化處理所以跑出來的結果不一定照順序
Example3:sections平行化每個section個別平行運算
#include<omp.h>
#include<stdio.h>
#include<stdlib.h>
void Test(int);int main()
{
#pragma omp parallel sections
{#pragma omp section
{
for(int i=0;i<100000;++i)
{}
printf("thread %d , first section\n",omp_get_thread_num());
}
#pragma omp section
{
printf("thread %d , second section\n",omp_get_thread_num());
}
#pragma omp section
{
printf("thread %d , third section\n",omp_get_thread_num());
}
#pragma omp section
{printf("thread %d , fourth section\n",omp_get_thread_num());
}
}
}
Output:因為第1個section跑比較久所以最後才顯示出來
Example4: single 只跑一次,master 只讓master thread跑
#include<omp.h>
#include<stdio.h>
#include<stdlib.h>int main()
{#pragma omp parallel
{
for(int i=0;i<3;i++)
{
for(int j=0;j<100000;j++){}
printf("thread %d : %d\n",omp_get_thread_num(),i);
}
printf("thread %d : two times\n",omp_get_thread_num());#pragma omp single
{
printf("thread %d : one times\n",omp_get_thread_num());
}#pragma omp master
{
printf("thread %d : master \n",omp_get_thread_num());
}
}
}
Output:被single包含的程式只執行一次,被master包含的程式只會讓master執行
Example5:被private包含的變數再跑平行運算時,每個thread會自己複製一份不會共用同一份變數
#include<omp.h>
#include<stdio.h>
#include<stdlib.h>int main()
{
int i,j;#pragma omp parallel for
for(i=0;i<5;i++)
for(j=0;j<5;j++)
{
printf("thread %d : %d loop\n",omp_get_thread_num(),i*5+j);
}
printf("--------------------------------------------------------\n");
#pragma omp parallel for private(j)
for(i=0;i<5;i++)
for(j=0;j<5;j++)
{
printf("thread %d : %d loop\n",omp_get_thread_num(),i*5+j);
}
}
Output:左邊因為共用變數j而導致迴圈沒跑滿25圈,右邊因為把j複製多份所以沒這問題
Example6:firstprivate 和private差不多只是在複製時也會複製初始值,lastprivate則是會在最後將複製出來的值丟回到本尊
#include <omp.h>
#include <stdio.h>struct counter {
int count;
};typedef struct counter counter;int main()
{
counter A;
A.count =-5;#pragma omp parallel for firstprivate(A) lastprivate(A)
for(int i=0;i<5;i++)
{
A.count++;
printf("Thread %d : count %d\n",omp_get_thread_num(),A.count);
}printf("final count %d\n",A.count);}
Output:因為firstprivate的關係所以count會從-5往上加,lastprivate則會把最後一條跑完的thread的count賦予到原本的A.count所以才是-3
Example7: atomic是為了保證變數在做計算時不被其他thread跟改到而導致計算出的東西有錯誤(race condition)
#include<omp.h>
#include<stdio.h>int main()
{
int sum =0;
#pragma omp parallel for
for(int i=0;i<1000;i++)
for(int j=0;j<5000;j++)
#pragma omp atomic
sum+=1;printf("%d\n",sum);}
Output:如果沒有加atomic跑出來的數字會是低於5,000,000,加了atomic可以保證變數做運算時不會被其他thread給更改到數字
Example8:reduction目的和上面很像,他是將每個sum依照thread各別複製一份出來後最後join時將所有sum相加就不會導致錯誤發生,但是只可以接受+、*、-、&...等運算符號
#include<omp.h>
#include<stdio.h>
#include<stdlib.h>int main()
{
int sum = 0;
double start = omp_get_wtime();
#pragma omp parallel for reduction(+:sum)
for(int i=0;i<1000;i++)
{
for(int j=0;j<5000;j++)
{
sum+=1;
}}printf("sum %d : time %4g second\n",sum,omp_get_wtime()-start);
}
Output:如果沒有加reduction出來的數字會因為race condition而有錯誤
Example9:schedule分成4種static,dynamic,guided,runtime,auto
static:將迴圈每n個分一組,依照thread順序輪流給每個thread執行,當跑過一輪後再從第一個thread開始輪流跑
schedule(static, 4):
**** **** **** ****
**** **** **** ****
**** **** **** ****
**** **** **** ****
dynamic:將迴圈每n個分一組,隨機分配給thread執行
schedule(dynamic, 4):
**** **** ****
**** **** **** **** ****
**** **** **** **** ****
**** **** ****
guilded:剛開始會依照thread數量下去切,如果迴圈有64個,thread有4個,那一開始第一組的數量則是64/4=16,依序往後每組數量會遞減,收縮到n個一組,如剩下的數量不夠n個則剩下的全部變成1組
schedule(guided, 4):
*******
************ **** ****
*********
**************** ***** **** ***
runtime:先不指定方法等到要執行時會依照系統變數OMP_SCHEDULE或omp_set_schedule做設定
schedule(runtime):
auto:由系統幫忙處理
schedule(auto):
schedule(static,4)範例
#include<omp.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
#pragma omp parallel for schedule(static,4) num_threads(2) ordered
for(int i=0;i<16;i++)
{
#pragma omp ordered
printf("Thread %d has completed iteration %d \n",omp_get_thread_num(),i);
}printf("All done!\n");return 0;}
Output:將迴圈每4個一組下去跑,每次跑的thread都會照順序,thread0先跑在換thread1,依此類推