この記事は、Go Parallel に掲載されている「How to Create Vectorized, Multicore Loops in OpenMP with Ease」 (http://goparallel.sourceforge.net/creating-vectorized-multicore-loops-openmp/) の日本語参考訳です。
OpenMP* のプラグマ宣言子を使用すると、1 つのループでベクトル化とマルチコア並列化の両方を適用することができます。この記事では、その方法を説明します。
Go Parallel を愛読されている方は、Go Parallel が並列プログラミングの 2 つの主要な分野 (マルチコア・プログラミングと SIMD ベクトル化) に注目していることをご存知でしょう。OpenMP* でループを並列化する場合、どちらも指定することができます。
すでにご存じのように、OpenMP* オプションによる並列化の制御はプラグマにより指定します。プラグマは、omp から始まり、指示句が続きます。指示句は、大文字と小文字が区別されます。
SIMD ベクトル化
ループをベクトル化するには、simd 構文を使用します。
#pragam omp simd
(注: simd 構文を使用するには、C++ コンパイラーで OpenMP* 4.0 がサポートされていなければなりません。インテル® C++ コンパイラー 14.0 以降はサポートしています。)
ベクトル化されたループは、それを作成した現在のスレッドで実行されます。各コアには個別の SIMD レジスターがあり、それぞれ別のベクトル化されたループをサポートできるため、これは重要です。(筆者がベクトル化について学んだとき、この点が疑問でしたが、前述のようにインテルのエンジニアが解決してくれました。)
以下に、simd プラグマの使用例を示します。
#include <iostream> #include <omp.h> using namespace std; int main() { const int SIZE = 300; int x[SIZE]; int y[SIZE]; int z[SIZE]; // ループの初期化 for (int i=0; i<SIZE; i++) { x[i] = i; y[i] = i * 2; z[i] = 0; } // メインループ #pragma omp simd for (int i=0; i<SIZE; i++) { z[i] = x[i] + y[i]; } return 0; }
このコードをインテル® コンパイラーでコンパイルするには、/Qopenmp オプションに加えて、/Qopenmp-simd オプションを指定する必要があります。(注: ドキュメントには、/Qopenmp オプションを指定するとデフォルトで SIMD が有効になると記述されていますが、筆者がテストした際は SIMD が有効にならなかったため、/Qopenmp-simd を追加しました。)
マルチコア並列化
複数のコアに for ループの実行を分配するには、次のプラグマを使用します。
#pragma omd parallel for
前述のコード例で “simd” を “parallel for” に変換するだけです。簡単でしょう。
マルチコア並列化とベクトル化の併用
2 つのプラグマを併用することで、マルチコアによる並列化とベクトル化の両方を行うことができます。ここでは、意図的に最初にマルチコアベクトル化、次にベクトル化の順で表記しています。ループを並列化する場合、最初に複数のコアに分配します。次に、それぞれのコアで並列に実行されるループの反復をベクトル化することができます。
インテル® コンパイラーでマルチコア並列化とベクトル化を併用するには、次のプラグマを使用します。
#pragma omp parallel for simd
そして、/Qopenmp と /Qopenmp-simd を指定してコンパイルします。
ベクトル化された関数
SIMD ベクトル化を使用すると、ループ内で数値演算を行う代わりに、それらを異なる関数内に配置することができます。そのためには、declare simd 構文を使用して関数をベクトル化された関数としてコンパイラーに通知します。
#pragma omp declare simd
この構文を追加し忘れても、コードは問題なくコンパイルし実行できます。ただし、その場合ベクトル化は行われません。これは、生成されるアセンブリー・コードで確認できます。/S オプションを指定してコンパイルすると、アセンブリー・コードを含む .asm ファイルが生成されます。declare simd 構文を使用しない場合、関数のアセンブリー・コードは次のようになります。
add ecx, edx mov eax, ecx ret
つまり、2 つの整数レジスターを加算しているだけです。declare simd 構文を追加すると、次のように関数はベクトル化された加算を行います。
paddd xmm0, xmm1 ret
まとめ
ここでは、OpenMP* による初歩的な並列化について説明しました。今後は、徐々にレベルを上げていくつもりです。例えば、OpenMP* により複数のループを 1 つに結合する方法を紹介します。simd 構文と parallel 構文にはいくつかのオプションがあり、コードのコンパイル方法を細かく制御できます。記事で紹介する例には、インテル® コンパイラーを使用しますが、GNU* コンパイラーと Microsoft* コンパイラーも OpenMP* をサポートしています。今後の記事にご期待ください。