OpenMP初探


OpenMP支持c、cpp、fortran,本文對比使用openmp和未使用openmp的效率差距和外在表現,然后講解基礎知識。

一、舉例

1、使用OpenMP與未使用OpenMP的比較。

OpenMP是使用多線程的接口。

以c語言程序舉例,即ba.c文件如下:

#include <omp.h>
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
void Test(int n)
{
    int j;
    for (int i = 0; i < 100000000; ++i)
    {
        //do nothing, just waste time
        j++;
    }
    printf("%d, ", n);
}
int main(int argc, char *argv[])
{
    int i;

    #pragma omp parallel for
    for (i = 0; i < 100; ++i)
        Test(i);
    
    system("pause");    
    return 1;
}

在編譯時,參數如下:

編譯結果如下:

耗時:9s

注意:我的電腦為雙核,所以開啟了4個線程分別運行

接下來,我通過window + R,輸入msconfig,並進入boot中的高級設置,將我電腦的設置為單核,然后再運行同樣的程序,可以發現結果如下:

於是可以發現,再設置為單核之后,程序會創建兩個線程,這樣的結果就是從0 50開始划分,這樣顯然是沒有充分利用cpu的,所以將電腦設置為原來的雙核。

 

 

 

未優化的如下:

// #include <omp.h>
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
void Test(int n)
{
    int j;
    for (int i = 0; i < 100000000; ++i)
    {
        //do nothing, just waste time
        j++;
    }
    printf("%d, ", n);
}
int main(int argc, char *argv[])
{
    int i;

    // #pragma omp parallel for
    for (i = 0; i < 100; ++i)
        Test(i);
    
    system("pause");    
    return 1;
}

在編譯時,參數如下:

編譯結果如下:

耗時: 24s

不難得知,此程序使用的為單核單線程 ,所以運行速度遠遠低於使用多核多線程的速度。

 

上面輸出了100個數字,時間上來說優化后是未優化的3倍。

然后后續我又輸出了1000個數字,未優化的時間為240s,而優化后的時間為84s,可見優化后同樣也是優化前的3倍左右。

之所以四個線程的速度僅僅是單線程速度的3倍而不是4倍,這是因為多線程在線程的切換、合作等方面也需要花費一定的時間,所以只是到了3倍的差距,而沒有達到4倍的差距。

 

 

2、獲取當前線程id、獲取總的線程數

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

int main(int argc, char *argv[])
{

    int nthreads,tid;

    // fork a team of thread
    #pragma omp parallel private(nthreads,tid)
    {
        //obtian and print thread id
        tid=omp_get_thread_num();
        printf("Hello Word from OMP thread %d\n",tid);

        // only master thread does this;
        if(tid==0)
        {
            nthreads = omp_get_num_threads();
            printf("Number of thread: %d\n",nthreads);
        }
    }
    
    system("pause");    
    return 1;
}

編譯條件如下:

 

運行結果如下:

每次運行,可以發現順序是不同的。但是Number of thread: 4永遠是在線程0之后出現,並且tid==0時的這個線程為主線程。

 

 

3、之前使用的為c語言,下面改寫為c++。

#include <omp.h>
#include <iostream>
#include <windows.h>
using namespace std;
void Test(int n)
{
    int j;
    for (int i = 0; i < 100000000; ++i)
    {
        //do nothing, just waste time
        j++;
    }
    cout << n << " ";
}
int main(int argc, char *argv[])
{
    int i;

    #pragma omp parallel for
    for (i = 0; i < 100; ++i)
        Test(i);

    system("pause");
    return 1;
}

編譯條件為 g++ t.cpp -o t -fopenmp,結果如下:

同樣地,這里使用四核來生成的。

 

 

4、如下所示,也是使用c++語言。 

#include <iostream>
#include <windows.h>
#include <omp.h>
using namespace std;
void test(int m) {
    int i = 0;
    double a = 0.0;
    double b = 0.0;
    double c = 0.0;
    for (i = 0; i < 100000000; i++) {
        a += 0.1;
        b += 0.2;
        c = a + b;
    }
    cout << m << " ";
}
int main()
{
    #pragma omp parallel for
    for (int i = 0; i < 200; i++) {
        test(i);
    }
    system("pause");
    return 0;
}

在這里也沒有什么很大的區別,總之,我們就是需要將void test這個函數寫的復雜一些,然后就會耗時,這樣才能看出來變化。

另外,在這里,我們可以看到結果中,是對for循環進行等量的划分,比如對於i從0到200的for循環里,會根據我電腦的2核划分為0 - 50、 51-100、 101-150、 151-200這幾個區間,然后使用多核cpu進行運算,這個優化的效果我想是非常驚人的。

 

 

 

 

5、三層循環

#include <iostream>
#include <windows.h>
#include <omp.h>
using namespace std;
void test(int m, int n, int l) {
    int i = 0;
    double a = 0.0;
    double b = 0.0;
    double c = 0.0;
    for (i = 0; i < 100000000; i++) {
        a += 0.1;
        b += 0.2;
        c = a + b;
    }
    cout << m << " " << n << " " << l  << endl;
}
int main()
{
    cout << "first" << endl;
    #pragma omp parallel for
    for (int i = 0; i < 5; i++) {
        for (int j = 0; j < 5; j++) {
            for (int k = 0; k < 5; k++) {
                test(i, j, k);
            }
        }
    }

    cout << "over hah" << endl;
    cout << "over hah" << endl;
    cout << "over hah" << endl;
    cout << "over hah" << endl;
    cout << "over hah" << endl;
    cout << "over hah" << endl;
    cout << "over hah" << endl;
    cout << "over hah" << endl;
    cout << "over hah" << endl;
    cout << "over hah" << endl;
    cout << "over hah" << endl;
    cout << "over hah" << endl;
    cout << "over hah" << endl;

    cout << "second" << endl;
    #pragma omp parallel for
    for (int i = 0; i < 5; i++) {
        for (int j = 0; j < 5; j++) {
            for (int k = 0; k < 5; k++) {
                test(i, j, k);
            }
        }
    }
    system("pause");
    return 0;
}

結果,

可以看到,還是最外層的循環進行轉化。

耗時: 45s。

 

如果我們將上面的兩個#pragma openmp parallel for去掉,再進行試驗。注意#pragma是預編譯指令,比如這里告訴編譯器要進行並行運算。

則需要100s左右。雖然沒有特別明顯的提高,但是還是快了很多,優勢是非常明顯的。

上面的舉例都是一些簡單的例子,而對於具體的項目還會遇到問題,需要靈活應變。

 

 

二、基礎

  需要使用openmp就需要引入omp.h庫文件。然后在編譯時添加參數 -fopenmp即可。 在具體需要進行並行運算的部分,使用 #pragma omp 指令[子句] 來告訴編譯器如何並行執行對應的語句。 常用的指令如下:

  • parallel - 即#pragma omp parallel 后面需要有一個代碼片段,使用{}括起來,表示會被並行執行。
  • parallel for - 這里后面跟for語句即可,不需要有額外的代碼塊。
  • sections
  • parallel sections
  • single - 表示只能單線程執行
  • critical - 臨界區,表示每次只能有一個openmp線程進入
  • barrier - 用於並行域內代碼的線程同步,線程執行到barrier時停下來 ,直到所有線程都執行到barrier時才繼續。

  常用的子句如下:

  • num_threads - 指定並行域內線程的數目
  • shared - 指定一個或者多個變量為多個線程的共享變量
  • private - 指定一個變量或者多個變量在每個線程中都有它的副本  

  另外,openmp還提供了一些列的api函數來獲取並行線程的狀態或控制並行線程的行為,常用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 - 返回系統中處理器的個數  

  

  

1、如下使用parallel,會根據電腦配置並行執行多次。

#include <iostream>
#include <windows.h>
#include <omp.h>
using namespace std;
int main()
{
    #pragma omp parallel
    {
        cout << "this is in parallel" << endl;
    }
    system("pause");
    return 0;
}

 

2、使用parallel num_threads(3),限制並行的線程數為3。

#include <iostream>
#include <windows.h>
#include <omp.h>
using namespace std;
int main()
{
    #pragma omp parallel num_threads(3)
    {
        cout << "this is in parallel" << endl;
    }
    system("pause");
    return 0;
}

這樣,最終會輸出3個語句,因為語句被並行運行了3次。結果如下:

但是上面的結果不是固定的,這里可以很明顯的表示出程序是並行運行的,因為第一個輸出還沒來得及換行,第二個又繼續輸出了,所以它們是獨立地並行地運算的。

 

 

3、下面我們使用 #pragma parallel for num_threads(4),並且在並行域中,我們還通過 omp_get_thread_num()來獲取線程號,如下:

#include <iostream>
#include <windows.h>
#include <omp.h>
using namespace std;
int main()
{
    #pragma omp parallel for num_threads(4)
    for (int i = 0; i < 20; i++) {
        cout << omp_get_thread_num() << endl;
    }
    system("pause");
    return 0;
}

這里就是對這個for循環使用4個線程來並行。 注意 #pragma omp parallel for num_threads(4) 與 #pragma omp parallel  num_threads(4) 不同,可自行體會。

結果如下,出現空行是因為多線程並行運算,導致換行符沒來得及輸出另外一個線程號就被輸出了。

 

 

4、對比單線程、2線程、4線程、...... 、12線程效率。

#include <iostream>
#include <windows.h>
#include <omp.h>
using namespace std;
void test() {
    int j = 0;
    for (int i = 0; i < 100000; i++) {
        // do something to kill time...
        j++;
    }
};

int main()
{

    double startTime;
    double endTime;

    // 不使用openMp
    startTime = omp_get_wtime();
    for (int i = 0; i < 100000; i++) {
        test();
    }
    endTime = omp_get_wtime();
    cout << "single thread cost time: " << endTime - startTime << endl;

    // 2個線程
    startTime = omp_get_wtime();
    #pragma omp parallel for num_threads(2)
    for (int i = 0; i < 100000; i++) {
        test();
    }
    endTime = omp_get_wtime();
    cout << "2 threads cost time: " << endTime - startTime << endl;

    // 4個線程
    startTime = omp_get_wtime();
    #pragma omp parallel for num_threads(4)
    for (int i = 0; i < 100000; i++) {
        test();
    }
    endTime = omp_get_wtime();
    cout << "4 threads cost time: " << endTime - startTime << endl;

    // 6個線程
    startTime = omp_get_wtime();
    #pragma omp parallel for num_threads(6)
    for (int i = 0; i < 100000; i++) {
        test();
    }
    endTime = omp_get_wtime();
    cout << "6 threads cost time: " << endTime - startTime << endl;

    // 8個線程
    startTime = omp_get_wtime();
    #pragma omp parallel for num_threads(8)
    for (int i = 0; i < 100000; i++) {
        test();
    }
    endTime = omp_get_wtime();
    cout << "8 threads cost time: " << endTime - startTime << endl;

    // 10個線程
    startTime = omp_get_wtime();
    #pragma omp parallel for num_threads(10)
    for (int i = 0; i < 100000; i++) {
        test();
    }
    endTime = omp_get_wtime();
    cout << "10 threads cost time: " << endTime - startTime << endl;

    // 12個線程
    startTime = omp_get_wtime();
    #pragma omp parallel for num_threads(12)
    for (int i = 0; i < 100000; i++) {
        test();
    }
    endTime = omp_get_wtime();
    cout << "12 threads cost time: " << endTime - startTime << endl;

    system("pause");
    return 0;
}

結果如下:

 

 於是,我們可以看到,單線程(不使用openMP)時消耗時間最長,2線程約為單線程的一半,4個線程(本電腦為4個邏輯內核)約為1/3時間,6個線程的時候時間甚至更長,12個線程在時間上也沒有明顯額減少,所以,線程數的制定可以根據電腦的核心數來做出選擇。

 

 

 

更多例子:

#include <iostream>
#include <windows.h>
#include <omp.h>
using namespace std;
void test(int i) {
    int j = 0;
    for (int i = 0; i < 100000000; i++) {
        // do something to kill time...
        j++;
    }
    cout << i << endl;
};

int main()
{

    // 此程序中使用到的openmp不可簡單地理解為一個封裝起來的庫,實際上應該理解為一個框架。這個框架是由硬件開發商和軟件開發商共同開發的,即通過協商api,來使得多核並行運算更容易上手、使用
    // 主要參考文章:https://wdxtub.com/2016/03/20/openmp-guide/
    
    // #pragma omp parallel for
    // for (int i = 0; i != 10; i++) {
    //     test(i);
    // }



    // #pragma omp parallel 
    // {
    //     #pragma omp for
    //     for (int i = 0; i < 10; i++) {
    //         test(i);
    //     }
    // }



    // XXX 錯誤,對於並行運算,不支持 != 的形式
    // #pragma omp parallel for
    // for (int i = 0; i != 10; i++) {
    //     test(i);
    // }



    // 在並行區域內聲明了一個變量private_a,那么在多線程執行時,每個線程都會創建這么一個private_a變量。
    // 最終輸出結果為668/668/669/669,說明2個線程加了兩次,2個線程加了3次。
    // #pragma omp parallel
    // {
    //     int private_a = 666;
    //     #pragma omp for
    //     for (int i = 0; i < 10; i++) {
    //         test(i);
    //         private_a++;
    //     }
    //     cout << private_a << endl;
    // }



    // 在並行區域之外定義的變量是共享的,即使下面有多個線程並行執行for循環,但是不會為每個線程創建share_a變量,所以最終每個線程訪問的都是同一個內存,輸出的結果為4個676
    // int share_a = 666;
    // #pragma omp parallel
    // {
    //     #pragma omp for
    //     for (int i = 0; i < 10; i++) {
    //         test(i);
    //         share_a++;
    //     }
    //     cout << share_a << endl;
    // }


    // 注意:這種循環是普通的循環,其中的sum是共享的,然后sum是累加的,所以從結果中也可以看出sum一定是非遞減的,最終結果為45。
    // int sum = 0;
    // cout << "Before: " << sum << endl;
    // #pragma omp parallel for
    // for (int i = 0; i < 10; i++) {
    //     sum = sum + i;
    //     cout << sum << endl;
    // }
    // cout << "After: " << sum << endl;


    // 注意:這里采用了reduction(+:sum),所以每個線程根據reduction(+:sum)的聲明計算出自己的sum(注意:在每個線程計算之初,sum均為在並行域之外規定的0,即對於4個線程而言,4個線程都會有一個初始值為0的sum,然后再疊加),然后再將各個線程的sum添加起來,所以從結果來看,sum是不存在某種特定規律的。
    // int sum = 0;
    // cout << "Before: " << sum << endl;
    // #pragma omp parallel for reduction(+:sum)
    // for (int i = 0; i < 10; i++) {
    //     sum = sum + i;
    //     cout << sum << endl;
    // }
    // cout << "After: " << sum << endl;


    // 下面的減法是類似的,對比上面的兩個例子即可。
    // int sum = 100;
    // cout << "Before: " << sum << endl;
    // #pragma omp parallel for
    // for (int i = 0; i < 10; i++) {
    //     sum = sum - i;
    //     cout << sum << endl;
    // }
    // cout << "After: " << sum << endl;

    // int sum = 100;
    // cout << "Before: " << sum << endl;
    // #pragma omp parallel for reduction(-:sum)
    // for (int i = 0; i < 10; i++) {
    //     sum = sum - i;
    //     cout << sum << endl;
    // }
    // cout << "After: " << sum << endl;



    // 下面的兩個例子中一個使用了原子操作,一個沒有使用原子操作。
    // 使用原子操作的最后結果正確且穩定,而沒有使用原子操作最終的結果是不穩定的。
    // int sum = 0;
    // cout << "Before: " << sum << endl;
    // #pragma omp parallel for
    // for (int i = 0; i < 20000; i++) {
    //     #pragma omp atomic
    //     sum++;
    // }
    // cout << "Atomic-After: " << sum << endl;

    // int sum = 0;
    // #pragma omp parallel for
    // for (int i = 0; i < 20000; i++) {
    //     sum++;
    // }
    // cout << "None-atomic-After: " << sum << endl;



    // 線程同步之critical
    // 使用critical得到的結果是穩定的,而不使用critical得到的結果是不穩定的。
    // 值得注意的是:critical與atomic的區別在於 - atomic僅僅使用自增(++、--等)或者簡化(+=、-=等)兩種方式,
    // 並且只能表示下一句,而critical卻沒有限制,且可以通過{}代碼塊來表示多句同時只能有一個線程來訪問。
    // int sum = 0;
    // cout << "Before: " << sum << endl;
    // #pragma omp parallel for
    // for (int i = 0; i < 100; i++) {
    //     #pragma omp critical(a)
    //     {
    //         sum = sum + i;
    //         sum = sum + i * 2;
    //     }
    // }
    // cout << "After: " << sum << endl;


    // 同時運行下面的兩個程序,可以發現有些許不同。
    // 這個程序中的第一個for循環會多線程執行,並且如果一個線程執行完,如果有的線程沒有執行完,
    // 那么就會等到所有線程執行完了再繼續向下執行。所以結果中 - 和 + 區分清晰。
    // #pragma omp parallel 
    // {
    //     #pragma omp for 
    //     for (int j = 666; j < 1000; j++) {
    //         cout << "-" << endl;
    //     }

    //     #pragma omp for nowait
    //     for (int i = 0; i < 100; i++) {
    //         cout << "+" << endl;
    //     }
    // }

    // 這個程序中的第一個for循環同樣會有多個線程同時執行,只是其中某個線程最先執行完了之后,
    // 不會等其他的線程,而是直接進入了下一個for循環,所以結果中的 - 和 + 在中間部分是混雜的。
    // #pragma omp parallel 
    // {
    //     #pragma omp for nowait
    //     for (int i = 0; i < 100; i++) {
    //         cout << "+" << endl;
    //     }

    //     #pragma omp for 
    //     for (int j = 666; j < 1000; j++) {
    //         cout << "-" << endl;
    //     }
    // }
    // 
    // 可知,barrier為隱式柵障,即並行區域中所有線程執行完畢之后,主線程才繼續執行。
    // 而nowait的聲明即可取消柵障,這樣,即使並行區域內即使所有的線程還沒有執行完,
    // 但是執行完了的線程也不必等待所有線程執行結束,而可自動向下執行。


    // 如下所示正常來說應該是第一個for循環中的一個線程執行完之后nowait進入下一個for循環,
    // 但是我們通過 #pragma omp barrier 來作為顯示同步柵障,即讓這個先執行完的線程等待所有線程執行完畢再進行下面的運算
    // #pragma omp parallel 
    // {
    //     #pragma omp for nowait
    //     for (int i = 0; i < 100; i++) {
    //         cout << "+" << endl;
    //     }

    //     #pragma omp barrier

    //     #pragma omp for 
    //     for (int j = 666; j < 1000; j++) {
    //         cout << "-" << endl;
    //     }
    // }



    // 這里我們通過#pragma omp master來讓主線程執行for循環,然后其他的線程執行后面的cout語句,
    // 所以,cout的內容會出現在for循環多次(這取決於你電腦的性能),最后,主線程執行完for語句后,也會執行一次cout
    // #pragma omp parallel
    // {
    //     #pragma omp master
    //     {
    //         for (int i = 0; i < 10; i++) {
    //             cout << i << endl;
    //         }
    //     }
    //     cout << "This will be shown two or more times" << endl;
    // }



    // 使用section可以指定不同的線程來執行不同的部分
    // 如下所示,通過#pragma omp parallel sections來指定不同的section由不同的線程執行
    // 最后得到的結果是多個for循環是混雜在一起的
    // #pragma omp parallel sections
    // {
    //     #pragma omp section
    //     for (int i = 0; i < 10; i++) {
    //         cout << "+";
    //     }

    //     #pragma omp section 
    //     for (int j = 0; j < 10; j++) {
    //         cout << "-";
    //     }

    //     #pragma omp section
    //     for (int k = 0; k < 10; k++) {
    //         cout << "*";
    //     }        
    // }

    system("pause");
    return 0;
}

通過上面的例子,我們就可以對OpenMP有一個基本的入門過程了。

 


免責聲明!

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



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