OpenMP並行編程

OpenMP並行編程

距離上次寫並行計算概述很有段日子了,後續的可能至少有三篇文章需要寫。今天花了點時間針對Openmp共享內存的並行編程寫了點東西。這裏只能提供一點概念和感覺(甚至有可能會誤導),需要系統學習Openmp並行計算的朋友可以到文章末尾下載推薦的教材。個人覺得把那書翻翻就足夠用啦~

  • 什麼是OpenMP?
    OpenMP (Open Multi-Processing) is an application programming interface (API) that supports multi-platform shared memory multiprocessing programming in C, C++ and Fortran on many architectures, including Unix and Microsoft Windows platforms. It consists of a set of compiler directives, library routines, and environment variables that influence run-time behavior. ”
    簡單來說,OpenMP是一個可以應用於多種平臺的共享內存式並行計算的接口。
  • Openmp的工作模式:
    Openmp的工作模式爲串行-並行-串行…。一開始的主線程是串行,當在需要並行的時候(這時候程序中應該有相應的Openmp指令語句),多個線程開始一起工作。若當前的並行塊結束(仍舊由相應的Opemp指令語句來控制)時,又重新回到單一的主線程。如此可往復繼續。在一個四核心cpu上運行Openmp程序(並行塊的線程數默認是核心數目,這裏即爲4個線程),程序處於主線程時cpu利用率爲100%,但是當程序進入並行塊時所有的核心都會參與進來,cpu利用率會達到400%。如果程序的主要運算部分都處於並行區域,則絕大部分時間cpu都處於400%的工作狀態,這樣便大大提高cpu的利用率。

    image
  • Openmp程序的結構:
    正如上面所說,編寫Openmp程序只需要在已有的串行程序上稍加修改即可:在並行開始和結束的地方加上Openmp語句引導並行的開始和結束。這些引導語句本身處於註釋語句的地位,必須在編譯時加上Openmp並行參數才能使其生效。如果不加編譯參數,編譯出來的程序仍舊是串行程序。
    Openmp是最容易實現的並行方式。
  • Openmp程序的編寫:
    下面以fortran語言爲例說明Openmp程序的編寫(對c語言和fortran語言,都可以參考本文最後給出的openmp教程)。一般的格式爲

    !$omp parallel CLAUSE
    !$omp DIRECTION
    [ structured block of code ]
    !$omp end DIRECTION
    !$omp end parallel

    其中DIRECTION是Openmp指令,有sections,do等,指定並行行爲。中間的

    [ structured block of code ]

    即是需要併線的程序塊。除了上面的稱爲DIRECTION的指令語句外,Openmp還需要稱爲CLAUSE的從句對並行進行限制和說明。比如,需要對私有變量進行聲明時就需要用到private從句(這是經常要遇到的,後面會以例子說明)。在fortran的串行編譯下,以“!”打頭的都處於屏蔽狀態是不起作用的。加了openmp編譯參數後纔會生效。

    我在程序編寫中用到最多的是do指令,偶爾用一下sections。do指令通常用來並行化do循環。本來用一個線程來執行的長的do循環被分割成幾個部分讓多個線程同時執行,這樣就節省了時間。sections指令通常用來將前後沒有依賴關係的程序塊(也即原本不分先後,你換下順序也無所謂)並行化。因爲無關聯,所以可以同時執行。

    可以說,若程序主要用來做計算,掌握了do和sections這兩個指令足矣!

  • 簡單的程序例子:
    1.Sections 指令的應用:

    !$OMP PARALLEL SHARED(A,B,C), PRIVATE(I)   !//Paralell塊開始
    !$OMP SECTIONS  !//Sections開始
    !$OMP SECTION     !//第一個section
    DO I = 1, N/2
    C(I) = A(I) + B(I)
    END DO
    !$OMP SECTION    !//第二個section
    DO I = 1+N/2, N
    C(I) = A(I) + B(I)
    END DO
    !$OMP END SECTIONS NOWAIT       !//Sections結束
    !$OMP END PARALLEL     !//Paralell塊結束

    這個並行語句將本來從1到N的循環手動分爲兩個部分並行執行。上面的shared,private就是從句(clause),聲明A,B,C爲公有的,而循環指標I是私有的。因爲兩個section同時執行,都會對I進行改變,所以兩個section的循環指標必須彼此獨立,不能是同一個變量。PRIVATE會自動將這個會引發衝突的變量按需生成多個拷貝以供使用。最後的!$OMP END SECTIONS NOWAIT語句告訴兩個線程可各自自行結束,無需相互等待。

    2.Do 指令的應用:
    上面用Section實現的功能完全可以用Do來實現:

    !$OMP PARALLEL SHARED(A,B,C), PRIVATE(I)    !//Paralell塊開始
    !$OMP DO    !//Do的並行開始
    DO I = 1, N
    C(I) = A(I) + B(I)
    END DO
    !$OMP END DO   !//Do的並行結束
    !$OMP END PARALLEL   !//Paralell塊結束

    Do循環本來是從1到N,現在有多少個線程就分爲多少個部分執行,比上面的section更方便智能。不用擔心循環次數N不能被線程數整除~。一般情況下,各個線程均分循環次數,但是在某些循環指標下運算可能比較快,所以各個線程的運算時間可能不盡相同。這時候如果需要讓各個線程都結束了才能再往下(沒有NOWAIT),快的線程就必須等待慢的線程。爲了解決這個問題需要加上schedule從句,首行變爲如下:

    !$OMP PARALLEL SHARED(A,B,C), PRIVATE(I),SCHEDULE(DYNAMIC)

    這個SCHEDULE(DYNAMIC)從句告訴程序動態調整併線方式,那些任務輕鬆運算快的線程會自動去幫任務重運算慢的線程,力爭所有線程同時完成任務。

    關於Do的積累計算,如累加,需要加上REDUCTION從句:

    C=0.d0
    !$OMP PARALLEL SHARED(A,C), PRIVATE(I),REDUCTION(+:C)
    !$OMP DO
    DO I = 1, N
    C =C+ A(I)
    END DO
    !$OMP END DO
    !$OMP END PARALLEL

    這裏將累加分爲幾個部分由多個線程進行運算,由於各個線程都在0.d0的基礎上開始計算它該算的部分,所以最後必須將各部分計算的結果再次求和。REDUCTION(+:C)從句就實現了這個效果。類似的疊乘等等用類似寫法,只需把“:”前的運算符改爲乘法“*”即可。

  • 一些注意問題:
    1.尤其要注意的問題就是變量的私有和公有問題。其實只要把握好一個原則,即如果這個變量有可能會被不同的線程同時進行寫操作(這不是你希望看到的),則這個變量就應該聲明爲私有。一般來說,並行體中臨時用到的一些中間變量應該是私有的。

    2.據我的經驗,Fortran中如果不特別聲明,變量都是默認公有的。這一點可以用DEFAULT(PRIVATE/SHARED)從句強行改變。循環指標默認是私有的,無需自己另外聲明。放在common域中的變量都是全局的,若要將這些全局變量私有化,可使用threadprivate指令(參見文章:OpenMP並行編程:threadprivate指令)。

    3.並行引導語句可以簡化,但要注意前後配對。比如上面那個累加的例子可以這樣寫:

    C=0.d0
    !$OMP PARALLEL DO SHARED(A,C), PRIVATE(I),REDUCTION(+:C)
    DO I = 1, N
    C =C+ A(I)
    END DO
    !$OMP END PARALLEL DO

    也即可以將從句加在指令之後。

    4.Fortran+Openmp的編譯問題:
    一般來說,加上-openmp編譯參數即可。如:
    ifort -openmp -o exe.out main.f
    gfortran用-fopenmp編譯參數,g77和ifort一樣用-openmp參數。
    如果用Makefile,將編譯參數放在合適的地方。

    5.對於多重do循環,如果中間變量太多,對私有公有弄不清楚或者雖然清楚但是閒麻煩,可以保留最外層循環,將裏面的循環在別處寫成一個子函數或子程序 ,然後在此處調用。這樣從結構上看就是對一重循環進行並行化,條理清楚不容易出錯。當然,傳遞給子函數或子程序的參數一般是要聲明私有的。

    6.將串行程序改爲Openmp並行程序後,在加與不加-openmp編譯參數的情況下分別編譯並運算,比較並行與串行的結果,確保並行塊沒有改錯。

    7.可以在並行開始前指定由多少個線程來並行。在單cpu單核心的機器上也可以(雖然沒有實際意義,但可以用來調試並行程序):

    CALL OMP_SET_NUM_THREADS(scalar_integer_expression)

    其中scalar_integer_expression是個整形變量,指定並行的線程數目。

    8.Openmp對私有變量的大小有限制。所以當遇到這樣的情況,一般就是由這個限制造成的:不加openmp並行時程序沒有問題,加了openmp並行時出現斷錯誤(segmentation fault),但是當把某個(一些)私有數組的維數變小時,段錯誤消失而且和串行時結果一致。
    解決辦法(linux下,windows下另外search)如下:
    在linux終端執行
    ulimit -s unlimited ;export KMP_STACKSIZE=2048000
    後一個數字參數足夠大即可。
     

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章