すべてまたは一部の元のループ反復が、ループ本体を実行していません。ピール/リマインダーループをループ本体へ移動することでパフォーマンスが改善されます。
データをアライメント
元のループ内のメモリーアクセスの一つが、 適切にアライメントされた境界から開始されていません。解決方法: データをアライメントし、コンパイラーにデータがアライメントされていることを知らせます。
データを 64 バイト境界にアライメントし、コンパイラーにデータがアライメントされていることを知らせます。
float *array; array = (float *)_mm_malloc(ARRAY_SIZE*sizeof(float), 32); // Somewhere else __assume_aligned(array, 32); // Use array in loop _mm_free(array);
静的データを 64 バイト境界でアライメントします。
__declspec(align(64)) float array[ARRAY_SIZE]
関連情報:
スレッド化と SIMD 命令の両方を使用してループを並列化
ループはスレッド化と自動ベクトル化されていますが、トリップカウントがベクトル長の倍数ではありません。解決方法: 次のすべてを行います。
オリジナルのコードサンプル:
void f(int a[], int b[], int c[]) { #pragma omp parallel for schedule(static) for (int i = 0; i < n; i++) { a[i] = b[i] + c[i]; } }
修正されたのコードサンプル:
void f(int a[], int b[], int c[]) { #pragma omp parallel for simd schedule(simd:static) for (int i = 0; i < n; i++) { a[i] = b[i] + c[i]; } }
関連情報:
スカラーのリマインダー生成を強制します
コンパイラーが生成したマスク付きのベクトル化されたリマインダーループは、効率良いベクトル化には反復数が少なすぎます。スカラーループがより適しています。解決方法: ディレクティブを使用してスカラー・リマインダーの生成を強制します。#pragma vector novecremainder を使用します。
add_floats(float *a, float *b, float *c, float *d, float *e, int n) { int i; // リマインダー・ループをベクトル化しないようにコンパイラーに強制 #pragma vector novecremainder for (i=0; i<n; i++) { a[i] = a[i] + b[i] + c[i] + d[i] + e[i]; } }
関連情報:
ベクトルリマインダーの生成を強制します
コンパイラーはリマインダー・ループをベクトル化しませんでしたが、パフォーマンスは向上します。解決方法: ディレクティブを使用してベクトル化を強制します。#pragma vector vecremainder を使用します。
add_floats(float *a, float *b, float *c, float *d, float *e, int n) { int i; // リマインダー・ループをベクトル化するようにコンパイラーに強制 #pragma vector vecremainder for (i=0; i<n; i++) { a[i] = a[i] + b[i] + c[i] + d[i] + e[i]; } }
関連情報:
推測されるループの反復数を指定
コンパイラーは静的にトリップカウントを認識できません。解決方法: ディレクティブを使用して推測される反復数を指定します。
#include <stdio.h> int mysum(int start, int end, int a) { int iret=0; // Iterate through a loop a minimum of three, maximum of ten, and average of five times #pragma loop_count min(3), max(10), avg(5) for (int i=start;i<=end;i++) iret += a; return iret; } int main() { int t; t = mysum(1, 10, 3); printf("t1=%d\r\n",t); t = mysum(2, 6, 2); printf("t2=%d\r\n",t); t = mysum(5, 12, 1); printf("t3=%d\r\n",t); }
関連情報:
チャンクサイズを変更します
ループは、#pragma omp parallel for simdディレクティブを使用してスレッド化とベクトル化されています。特に、このディレクティブはループ反復をチャンク (サブセット) に分割して、チャンクをスレッドに分配することで、チャンクの反復は SIMD 命令を使用して同時に実行されます。この場合、チャンクサイズ (チャンクごとの反復数) がベクトル長の倍数ではありません。解決方法: ベクトル宣言にschedule (simd: [kind])修飾子を#pragma omp parallel for simdディレクティブに追加します。
void f(int a[], int b[], int[c]) { // Guarantee a multiple of vector length.#pragma omp parallel for simd schedule(simd: static) for (int i = 0; i < n; i++) { a[i] = b[i] + c[i]; } }
関連情報:
データのパディングを行います
トリップカウントがベクトル長の倍数ではありません。解決方法: 次のいずれかを行います。
関連情報:
トリップ・カウント・データを収集
サーベイレポートで、正確な推奨を作成するにはトリップ・カウント・データが不足しています。
アンロールを無効にします
ループアンロール後のトリップカウントは、ベクトル長と比べると小さすぎです。解決方法: ループのアンロールを行わないか、ディレクティブを使用してアンロール係数を減らします。#pragma nounroll または #pragma unroll。
void nounroll(int a[], int b[], int c[], int d[]) { // Disable automatic loop unrolling using #pragma nounroll for (int i = 1; i < 100; i++) { b[i] = a[i] + 1; d[i] = c[i] + 1; } }
関連情報:
小さなベクトル長を使用します
コンパイラーはベクトル長を選択しましたが、トリップカウントはベクトル長よりも小さめです。解決方法: ディレクティブを使用して小さなベクトル長を指定します。#pragma omp simd simdlen を使用します。
void f(int a[], int b[], int c[], int d[]) { // Specify vector length using #pragma omp simd simdlen(4) for (int i = 1; i < 100; i++) { b[i] = a[i] + 1; d[i] = c[i] + 1; } }
インテル® Fortran コンパイラーのバージョン 19.0 以降には、コンパイラーがコストに基づいて最適なベクトル長を選択できるように指示する新しい vectorlength 句があります。#pragma VECTOR VECTORLENGTH (vl1, vl2, ..., vln)説明:vlは、2 の整数乗です。
void f(int a[], int b[], int c[], int d[]) { // ベクトル長のリストを指定 #pragma vector vectorlength(2, 4, 16) for (int i = 1; i < 100; i++) { b[i] = a[i] + 1; d[i] = c[i] + 1; } }
関連情報:
動的なアライメントを無効にします
コンパイラーは、ベクトルループをメモリー参照に合わせて、反復をベクトルループからスカラーループに自動的にピールします。ただし、この最適化が常に最適であるとは限りません。パフォーマンスを向上するには、次のディレクティブを使用して自動ピールを無効にします。#pragma vector nodynamic_align を使用します。
...#pragma vector nodynamic_align for (int i = 0; i < len; i++) ... void f(float * a, float * b, float * c, int len) { #pragma vector nodynamic_align for (int i = 0; i < len; i++) { a[i] = b[i] * c[i]; } }
関連情報:
ユーザー定義関数内のループ本体はベクトル化されません。
インライン展開を有効にします
ユーザー定義関数のインライン展開はコンパイラー・オプションで無効化されています。解決方法: インライン展開を制御するため、Ob または inline-levelコンパイラー・オプションを使用している場合、引数0を引数1に置き換えて、inline キーワードや属性で指定されるインライン展開を有効にするか、引数2で、すべての関数のインライン展開をコンパイラーの判断に任せます。
Windows* |
Linux* |
---|---|
/Ob1 または /Ob2 | -inline-level=1 または -inline-level=2 |
関連情報:
ループ内のシリアル化された関数をベクトル化します
#pragma omp declare simd int f (int x) { return x+1; } #pragma omp simd for (int k = 0; k < N; k++) { a[k] = f(k); }
関連情報:
ループ本体内の数学関数呼び出しは、コンパイラーがループを効率良くベクトル化するのを妨げます。ベクトル数学関数を使用することでパフォーマンスを改善します。
インライン展開を有効にします
インライン展開はコンパイラーによって無効化されています。解決方法: インライン展開を制御するため、Obまたはinline-levelコンパイラー・オプションを使用している場合、引数0を引数1に置き換えて、inlineキーワードや属性で指定されるインライン展開を有効にするか、引数2で、すべての関数のインライン展開をコンパイラーの判断に任せます。
Windows* |
Linux* |
---|---|
/Ob1 または /Ob2 | -inline-level=1 または -inline-level=2 |
あるいは、#include <mathimf.h>ヘッダーを標準の#include <math.h>に置き替えて、浮動小数点計算を行うアプリケーションで一般的に使用される最適化された高精度の数学関数を呼び出します。
関連情報:
ループ内の数学関数呼び出しをベクトル化します
浮動小数点モデル precise を使用すると、アプリケーションはシリアル化された数学関数を呼び出します。解決方法: 次のいずれかを行います。
Windows* |
Linux* |
---|---|
/Qfast-transcendentals | -fast-transcendentals |
void add_floats(float *a, float *b, float *c, float *d, float *e, int n){ int i; #pragma omp simd for (i=0; i<n; i++){ a[i] = a[i] + b[i] + c[i] + d[i] + e[i]; } }
関連情報:
浮動小数点モデルを変更します
strict 浮動小数点モデルを使用すると、アプリケーションはシリアル化された数学関数を呼び出します。strict浮動小数点モデルを使用すると、アプリケーションはシリアル化された数学関数を呼び出します。解決方法: 次のいずれかを行います。
Windows* OS |
Linux* |
---|---|
/fast | -fp-model fast |
/fp:precise /Qfast-transcendentals | -fp-model precise -fast-transcendentals |
gcc program.c -O2 -fopenmp -fp-model precise -fast-transcendentals #pragma omp simd collapse(2) for (i=0; i<N; i++) { a[i] = b[i] * c[i]; for (i=0; i<N; i++) { d[i] = e[i] * f[i]; } }
関連情報:
Glibc ライブラリーとベクトル化された SVML 関数を使用します
アプリケーションは、ベクトル化されたバージョンの数学関数ではなく、スカラー関数を呼び出しています。解決方法: 次のすべてを行います。
gcc program.c -O2 -fopenmp -ffast-math -lrt -lm -mavx2 -I/opt/glibc-2.22/include -L/opt/glibc-2.22/lib -Wl,--dynamic-linker=/opt/glibc-2.22/lib/ld-linux-x86-64.so.2 #include "math.h" #include "stdio.h" #define N 100000 int main() { double angles[N], results[N]; int i; srand(86456); for (i = 0; i < N; i++) { angles[i] = rand(); } #pragma omp simd for (i = 0; i < N; i++) { results[i] = cos(angles[i]); } return 0; }
関連情報:
ベクトル組込み関数にインテルの SVML を使用
アプリケーションは、ベクトル化されたバージョンの数学関数ではなく、スカラー関数を呼び出しています。解決方法: 次のすべてを行います。
gcc program.c -O2 -ftree-vectorize -funsafe-math-optimizations -mveclibabi=svml -L/opt/intel/lib/intel64 -lm -lsvml -Wl,-rpath=/opt/intel/lib/intel64 #include "math.h" #include "stdio.h" #define N 100000 int main() { double angles[N], results[N]; int i; srand(86456); for (i = 0; i < N; i++) { angles[i] = rand(); } // ループは自動ベクトル化されます for (i = 0; i < N; i++) { results[i] = cos(angles[i]); } return 0; }
関連情報:
コンパイラーがベクトル操作で使用するデータの間接、または不規則なストライドアクセスを想定しました。次のような通常のストライド・アクセス・パターンを検出するため、コンパイラーに通知することでメモリーアクセスを改善します。
パターン |
説明 |
---|---|
不変 | 命令はループ全体で同じメモリーの値にアクセスします。 |
均一 (水平方向は不変) | 命令はベクトル反復で同じメモリーの値にアクセスします。 |
垂直方向は不変 | 命令はすべてのベクトル反復間で同じオフセットを使用してメモリー位置にアクセスします。 |
ユニット | 命令はループ全体でベクトル反復 = ベクトル長のストライドで連続したメモリーの値をアクセスします。 |
一定 (非ユニット) | 命令は反復間で同じストライドを使用してメモリー位置にアクセスします。 |
検出された通常のストライド・アクセス・パターンでコードをリファクタリングします
メモリー・アクセス・パターン・レポートは次の通常ストライドアクセスを示します。
変数 |
パターン |
---|---|
block 0x2b05c877040 allocated at main.cpp:14 | ユニット |
block 0x2b05c877040 allocated at main.cpp:14 | 一定 (非ユニット) |
メモリー・アクセス・パターン・レポートのソース詳細表示を参照します。
メモリーアクセスを改善するには、次の操作を行います。コードをリファクタリングして、コンパイラーに通常のストライドアクセスであることを通知します。時に、複数ファイルにわたるプロシージャー間の最適化 (IPO) を有効にする、ipo/Qipoコンパイラー・オプションから利点が得られることがあります。
配列は、通常のインデックスでアクセス可能な、連続したデータ項目の集合を含む最も一般的なデータ構造です。構造体配列 (AoS)、または配列構造体 (SoA) として構成することができます。検出された定数ストライドは、AoS 実装によるものである可能性があります。この構成はカプセル化には最適ですが、効率良いベクトル処理の妨げになることがあります。解決方法: AoS の代わりに SoA を使用するデータ構成にコードを変更します。
しかし、AoS から SoA への移行コストがパフォーマンスの利点を上回る可能性があります。解決方法: インテル® C++ コンパイラーのバージョン 16.1 で導入されたインテル® SIMD Data Layout Templates (SDLT) を使用して、移行のコストを軽減できます。SDLT は、C++11 テンプレート・ライブラリーであり、コードの変更が数行で済みます。
垂直不変パターンのリファクタリング。
// main.cpp int a[8] = {1,0,5,7,4,2,6,3}; // gather.cpp void test_gather(int* a, int* b, int* c, int* d) { int i, k; // 非効率なアクセス #pragma omp simd for (i = 0; i < INNER_COUNT; i++) d[i] = b[a[i%8]] + c[i]; int b_alt[8]; for (k = 0; k < 8; ++k) b_alt[k] = b[a[k]]; // 効率良いバージョン for (i = 0; i < INNER_COUNT/8; i++) { #pragma omp simd for(k = 0; k < 8; ++k) d[i*8+k] = b_alt[k] + c[i*8+k]; } }ベクトル関数節がループ内の呼び出しの引数と一致していることを確認します (存在する場合)。
関数呼び出しと宣言を比較します。
// functions.cpp #pragma omp declare simd int foo1(int* arr, int idx) { return 2 * arr[idx]; } #pragma omp declare simd uniform(arr) linear(idx) int foo2(int* arr, int idx) { return 2 * arr[idx]; } #pragma omp declare simd linear(arr) uniform(idx) int foo3(int* arr, int idx) { return 2 * arr[idx]; } // gather.cpp void test_gather(int* a, int* b, int* c) { int i, k; // 複雑なアクセスパターンを関数呼び出しで使用できるよう、ループはベクトル化されます。#pragma omp simd for (i = 0; i < INNER_COUNT; i++) a[i] = b[i] + foo1(c,i); // ループはベクトル化された呼び出しでベクトル化されます #pragma omp simd for (i = 0; i < INNER_COUNT; i++) [i] = b[i] + foo2(c,i); // ループはシリアル呼び出しでベクトル化されます #pragma omp simd for (i = 0; i < INNER_COUNT; i++) a[i] = b[i] + foo3(c,i); }
関連情報:
レジスターのスピルが検出されました。すべてのベクトルレジスターが使用中です。これは、スピルされた変数はメインメモリーに退避/復帰されるため、パフォーマンス上悪影響があります。ベクトルレジスターへの圧力を緩和してパフォーマンスを改善します。
アンロール係数を減らします
現在のディレクティブのアンロールの係数は、レジスターへの圧力を高めます。解決方法: ディレクティブを使用してアンロール係数を減らします。#pragma nounrollまたは#pragma unroll。
void nounroll(int a[], int b[], int c[], int d[]) { #pragma nounroll for (int i = 1; i < 100; i++) { b[i] = a[i] + 1; d[i] = c[i] + 1; } }
関連情報:
ループを小さなループに分割します
高いレジスターへの圧力は、レジスタースピルの可能性を高め、効率良いベクトル化を妨げます。解決方法: ディレクティブ#pragma distribute_pointを使用するか、コードを書き換えてループを分割します。これは、レジスターへの圧力を低くしてソフトウェア・パイプライン処理を有効にすることで、命令とデータキャッシュ両方の使用を改善できます。
#define NUM 1024 void loop_distribution_pragma2( double a[NUM], double b[NUM], double c[NUM], double x[NUM], double y[NUM], double z[NUM] ) { int i; // ループの分割または分割後 for (i=0; i< NUM; i++) { a[i] = a[i] + i; b[i] = b[i] + i; c[i] = c[i] + i; x[i] = x[i] + i; y[i] = y[i] + i; z[i] = z[i] + i; } }
関連情報:
コンパイラーが、ループ内でアンチ依存関係 (読み取り後の書き込み - WAR) または真の依存関係 (書込み後の読み取り - RAW) を想定しました。想定を調査して適切に処理することでパフォーマンスを改善します。
実際に依存関係があるか確認します
実際に依存関係がループ中にあるか確認できません。確認方法: 依存関係解析を実行します。
ベクトル化を有効にします
依存関係解析は与えられたワークロードでループの依存関係を検出しませんでした。キーワードrestrictまたは、ディレクティブを使用してベクトル化が安全であることをコンパイラーに通知します。
Target |
ディレクティブ |
---|---|
#pragma omp simd | ループのすべての依存関係を無視。 |
#pragma ivdep | ベクトルの依存関係のみを無視 (最も安全) |
#pragma ivdep for (i = 0; i < n - 4; i += 4) { // ここに簡単なサンプルコードを示すコメント行が // もう 1 行あります ... a[i + 4] = a[i] * c; }
関連情報:
コンパイラーが、ループ内でアンチ依存関係 (読み取り後の書き込み - WAR) または真の依存関係 (書込み後の読み取り - RAW) を想定しました。想定を調査して適切に処理することでパフォーマンスを改善します。
依存関係の解決
依存性解析は、ループ内に実際の依存関係があることを示しています。解決方法: 次のいずれかを行います。
#pragma omp simd safelen(4) for (i = 0; i < n - 4; i += 4) { a[i + 4] = a[i] * c; }
#pragma omp simd reduction(+:sumx) for (k = 0;k < size2; k++) { sumx += x[k]*b[k]; }
関連情報:
ループ内に複数のデータ型が混在しています。データ型の変換を防ぐことでハードウェアのベクトル化サポートの効率を高めます。
最も小さなデータ型を使用します
ループは異なる幅のデータ型を含んでいます。解決方法: ベクトルレジスター幅全体を使用するため、必要な精度を提供する最小のデータ型を使用します。
例: 16 ビットのみ必要な場合、int より short を使用すると、4 ウェイと 8 ウェイの SIMD 並列処理のそれぞれ違いが実感できます。
ループ本体内のユーザー関数呼び出しは、コンパイラーがループをベクトル化するのを妨げます。
インライン展開を有効にします
ユーザー定義関数のインライン展開はコンパイラー・オプションで無効化されています。解決方法: インライン展開を制御するため、Obまたはinline-levelコンパイラー・オプションを使用している場合、引数0を引数1に置き換えて、inlineキーワードや属性で指定されるインライン展開を有効にするか、引数2で、すべての関数のインライン展開をコンパイラーの判断に任せます。
Windows* |
Linux* |
---|---|
/Ob1 または /Ob2 | -inline-level=1 または-inline-level=2 |
関連情報:
ループ内のユーザー関数をベクトル化します
コンパイラーはこれらのユーザー定義関数をベクトル化またはインライン展開できません:my_calc()解決方法: 次のいずれかを行います。
Target | ディレクティブ |
---|---|
元のループ | #pragma omp simd |
内部関数定義や宣言 | #pragma omp declare simd |
#pragma omp declare simd int f (int x) { return x+1; } #pragma omp simd for (int k = 0; k < N; k++) { a[k] = f(k); }
関連情報:
原因: インテル® コンパイラーを使用していないか、古いバージョンのインテル® コンパイラーを使用しています。それにもかかわらず、ベクトル化を妨げる問題はなく、ベクトル化が有益であるように示されることがあります。
ベクトル化の可能性を調査
自動ベクトル化を有効にしてコンパイルしても、コンパイラーはコードをベクトル化できないことがあります。ベクトル化の可能性を調査:
関連情報:
自動ベクトルを有効にします
自動ベクトル化を無効にしてコンパイルしています; 自動ベクトル化を有効にします。
関連情報:
ループ本体内のシステム関数呼び出しは、コンパイラーがループをベクトル化するのを妨げます。
システム関数呼び出しをループから排除します
一般にシステム関数やサブルーチン呼び出しはベクトル化されません; print 文はベクトル化を妨げる代表例です。解決方法: ループ内ではシステム関数の呼び出しを避けます。
ループ本体内の OpenMP* 関数呼び出しは、コンパイラーがループを効率良くベクトル化するのを妨げます。
OpenMP* API 呼び出しをループ本体の外へ移動します
不変でないなどの理由によって、コンパイラーが OpenMP* 呼び出しをループ本体の外へ移動できない場合、OpenMP* 呼び出しは自動ベクトル化を妨げます。解決方法:
Target |
ディレクティブ |
---|---|
その他 | #pragma omp parallel [clause, clause, ...] |
内部 | #pragma omp for [clause, clause, ...] |
オリジナルコードの例:
#pragma omp parallel for private(tid, nthreads) for (int k = 0; k < N; k++) { tid = omp_get_thread_num(); // ループ内のこの呼び出しはベクトル化を妨げます nthreads = omp_get_num_threads(); // ループ内のこの呼び出しはベクトル化を妨げます ...}
修正されたコードの例:
#pragma omp parallel private(tid, nthreads) { // OpenMP* 呼び出しをここへ移動します tid = omp_get_thread_num(); nthreads = omp_get_num_threads(); #pragma omp for nowait for (int k = 0; k < N; k++) { ...} }
関連情報:
OpenMP* ロック関数を排除します
オブジェクトのロックはループの実行を低速にします。解決方法: OpenMP* ロック関数を使用しないようにコードを書き換えます。
それぞれのスレッドに個別の配列を割り当てて、並列処理の後にそれらをマージすることで、速度が向上する可能性があります (ただし、メモリー消費量が増加します)。
オリジナルコードの例:
int A[n]; list<int> L; ... omp_lock_t lock_obj; omp_init_lock(&lock_obj); #pragma omp parallel for shared(L, A, lock_obj) default(none) for (int i = 0; i < n; ++i) { // A[i] calculation ... if (A[i]<1.0) { omp_set_lock(&(lock_obj)); L.insert(L.begin(), A[i]); omp_unset_lock(&(lock_obj)); } } omp_destroy_lock(&lock_obj);
修正されたコードの例:
int A[n]; list<int> L; omp_set_num_threads(nthreads_all); ... vector<list<int>> L_by_thread(nthreads_all); // スレッドごとの個別のリスト #pragma omp parallel shared(L, L_by_thread, A) default(none) { int k = omp_get_thread_num(); #pragma omp for nowait for (int i = 0; i < n; ++i) { // A[i] calculation ... if (A[i]<1.0) { L_by_thread[k].insert(L_by_thread[k].begin(), A[i]); } } } // merge data into single list for (int k = 0; k < L_by_thread.size(); k++) { L.splice(L.end(), L_by_thread[k]); }
関連情報:
非効率なメモリー・アクセス・パターンは、重要なベクトルコードの実行速度を低下させたり、コンパイラーによる自動ベクトル化の妨げとなります。調査してパフォーマンスを改善します。
非効率なメモリー・アクセス・パターンを確認します
非効率なメモリー・アクセス・パターンをの存在を確認する方法がありません。解決方法: メモリー使用とトラフィックを調査。
不規則な (可変またはランダム) ストライドアクセスのメモリーアクセス命令の比率が高いことが観測されました。調査して適切に処理することでパフォーマンスを改善します。
ループの並べ替え
このループは近接する外部ループほどメモリーのアクセス効率は良くありません。解決方法: 可能であればループを並べ替えます。
オリジナルコードの例:
void matmul(float *a[], float *b[], float *c[], int N) { for (int i = 0; i < N; i++) for (int j = 0; j < N; j++) for (int k = 0; k < N; k++) c[i][j] = c[i][j] + a[i][k] * b[k][j]; }修正されたコードの例:
void matmul(float *a[], float *b[], float *c[], int N) { for (int i = 0; i < N; i++) for (int k = 0; k < N; k++) for (int j = 0; j < N; j++) c[i][j] = c[i][j] + a[i][k] * b[k][j]; }依存関係があるため、入れ替えが常に可能であるとは限らず、異なる結果を招く可能性があります。
インテル® SDLT を使用します
AoS から SoA への移行コストがパフォーマンスの利点を上回る可能性があります。解決方法: コストを軽減するには、インテル® SIMD データ・レイアウト・テンプレート (インテル® SDLT) を使用します。SDLT は、C++11 テンプレート・ライブラリーであり、コードの変更が数行で済みます。
STL コンテナーに代わり SDLT を使用することで、効率良いベクトル処理向けにメモリー・アクセス・パターンを改善します。
オリジナルコードの例:
struct kValues { float Kx; float Ky; float Kz; float PhiMag; }; std::vector<kValues> dataset(count); // 初期化ステップ for(int i=0; i < count; ++i) { kValues[i].Kx = kx[i]; kValues[i].Ky = ky[i]; kValues[i].Kz = kz[i]; kValues[i].PhiMag = phiMag[i]; } // 計算ステップ for (indexK = 0; indexK < numK; indexK++) { expArg = PIx2 * (kValues[indexK].Kx * x[indexX] + kValues[indexK].Ky * y[indexX] + kValues[indexK].Kz * z[indexX]); cosArg = cosf(expArg); sinArg = sinf(expArg); float phi = kValues[indexK].PhiMag; QrSum += phi * cosArg; QiSum += phi * sinArg; }
修正されたコードの例:
#include <sdlt/sdlt.h> struct kValues { float Kx; float Ky; float Kz; float PhiMag; }; SDLT_PRIMITIVE(kValues, Kx, Ky, Kz, PhiMag) sdlt::soa1d_container<kValues> dataset(count); // 初期化ステップ auto kValues = dataset.access(); for (k = 0; k < numK; k++) { kValues [k].Kx() = kx[k]; kValues [k].Ky() = ky[k]; kValues [k].Kz() = kz[k]; kValues [k].PhiMag() = phiMag[k]; } // 計算ステップ auto kVals = dataset.const_access(); #pragma omp simd private(expArg, cosArg, sinArg) reduction(+:QrSum, QiSum) for (indexK = 0; indexK < numK; indexK++) { expArg = PIx2 * (kVals[indexK].Kx() * x[indexX] + kVals[indexK].Ky() * y[indexX] + kVals[indexK].Kz() * z[indexX]); cosArg = cosf(expArg); sinArg = sinf(expArg); float phi = kVals[indexK].PhiMag(); QrSum += phi * cosArg; QiSum += phi * sinArg; }
関連情報:
AoS の代わりに SoA を使用します
配列は、通常のインデックスでアクセス可能な、連続したデータ項目の集合を含む最も一般的なデータ構造です。構造体配列 (AoS)、または配列構造体 (SoA) として構成することができます。AoS はカプセル化には最適ですが、ベクトル化処理の妨げになることがあります。解決方法: AoS の代わりに SoA を使用するデータ構成にコードを変更します。
関連情報:
現在のハードウェアは、乗算加算融合 (FMA) 命令が利用できる AVX2 命令セットアーキテクチャー (ISA) をサポートします。FMA 命令を使用してパフォーマンスを改善します。
可能であればベクトル化を強制します
ループには FMA 命令 (ベクトル化の利点が考えられる) が含まれていますが、ベクトル化されていません。解決するには、以下を確認します。
関連情報:
strict 浮動小数点モデルを適用する場合、FMA 命令の生成を可能にします
静的解析は、ループが AVX2 ISA で使用可能な FMA 命令から利益が得られると推測しましたが、strict浮動小数点モデルは、デフォルトで FMA 命令の生成を無効にします。解決方法: この動作をfmaコンパイラー・オプションを使用して変更します。
Windows* | Linux* |
---|---|
/Qfma | -fma または /Qfma |
関連情報:
より上位の ISA ターゲット
静的解析は、ループがインテル® AVX2 以降で使用可能な FMA 命令から利点が得られると推測しましたが、このループでは FMA 命令が実行されていません。解決方法: インテル® AVX2 固有コードを生成するには、
関連情報:
xHost オプションの代わりに固有の ISA をターゲットにします
静的解析は、ループがインテル® AVX2 以降で使用可能な FMA 命令から利点が得られると推測しましたが、このループでは FMA 命令が実行されていません。解決方法: ホスト ISA による最適化の可能性を制限する xHost コンパイラー・オプションを使用する代わりに、次のコンパイラー・オプションを使用します。
Windows* | Linux* |
---|---|
/QxCORE-AVX2 または /QaxCORE-AVX2 | -xCORE-AVX2 または -axCORE-AVX2 |
/QxCOMMON-AVX512 または /QaxCOMMON-AVX512 | -xCOMMON-AVX512 または -axCOMMON-AVX512 |
関連情報:
ループ本体内の間接関数呼び出しは、コンパイラーがループをベクトル化するのを妨げます。間接呼び出しは、間接ジャンプとも呼ばれ、レジスターまたはメモリーから呼び出し先のアドレスを取得するのに対し、直接呼出しは引数から呼び出し先のアドレスを取得します。ループのベクトル化を強制しても間接呼び出しはシリアル化されます。
分岐予測の改善
64 ビット・アプリケーションでは、分岐ターゲットが分岐から 4 GB 以上離れている場合、分岐予測のパフォーマンスに悪影響を与えます。これは、アプリケーションが共有ライブラリーと分離されている場合に発生する可能性があります。解決方法: 次のことを行います。
関連情報:
ループ内の間接呼び出しを排除します
間接関数やサブルーチン呼び出しはベクトル化できません。解決方法: ループ内では間接呼び出しを避けます。
仮想メソッド呼び出しを直接呼出しに置き換えます
関数のアドレスがランタイムで計算されるため、仮想メソッド呼び出しは常に間接呼び出しとなります。解決するには次を行います。
Target |
ディレクティブ |
---|---|
元のループ | #pragma omp simd |
内部関数定義や宣言 | #pragma omp declare simd |
オリジナルコードの例:
struct A { virtual double foo(double x) { return x+1; } }; struct B : public A { double foo(double x) override { return x-1; } }; ...A* obj = new B(); double sum = 0.0; #pragma omp simd reduction(+:sum) for (int k = 0; k < N; ++k) { // 仮想間接呼び出し sum += obj->foo(a[k]); } ...
修正されたコードの例:
struct A { // インテル® コンパイラー 17.x 以降では、仮想メソッドの呼び出しをベクトル化できる可能性があります #pragma omp declare simd virtual double foo(double x) { return x+1; } }; ... sum = 0.0; #pragma omp simd reduction(+:sum) for (int k = 0; k < N; ++k) { // インテル® コンパイラー 16.x 以前の手順: // 呼び出されるメソッドが判明している場合、 // 仮想呼び出しを直接呼び出しに置き換えます sum += ((B*)obj)->B::foo(a[k]); } ...
関連情報:
仮想メソッド呼び出しのベクトル化
SIMD 命令を使用して元のループのベクトル化を強制し、および/またはディレクティブを使用してベクトルバージョンの関数を生成します。
Target |
ディレクティブ |
---|---|
元のループ | #pragma omp simd |
内部関数定義や宣言 | #pragma omp declare simd |
オリジナルコードの例:
struct A { virtual double foo(double x) { return x+1; } }; struct B : public A { double foo(double x) override { return x-1; } }; ...A* obj = new B(); double sum = 0.0; #pragma omp simd reduction(+:sum) for (int k = 0; k < N; ++k) { // 仮想メソッドの間接呼び出し sum += obj->foo(a[k]); } ...
修正されたコードの例:
struct A { #pragma omp declare simd virtual double foo(double x) { return x+1; } }; ...
関連情報:
SIMD 対応関数のデフォルトのベクトル宣言では、余分な計算や非効率なメモリー・アクセス・パターンをもたらす可能性があります。適切な節を追加してパフォーマンスを改善します。
固有のプロセッサー・タイプをターゲットとします
SIMD 対応関数のデフォルト命令セット・アーキテクチャー (ISA) は、レジスター間との余分なメモリーアクセスが生じる可能性があるため、ホスト・プロセッサーでは非効率です。解決方法: 次のいずれかを追加してコンパイラーにベクトル関数の拡張セットを生成することを通知します。
Windows* | Linux* |
---|---|
#pragma omp declare simd の processor(cpuid) | #pragma omp declare simd の processor(cpuid) |
_declspec(vector()) の processor(cpuid) | _attribute_(vector()) の processor(cpuid) |
/Qvecabi:cmdtarget 注意: コンパイラー・オプション /Qx または /Qax によって指定されるターゲットに対応する複数のベクトル関数が生成されます。 | -vecabi=cmdtarget 注意: コンパイラー・オプション -x または -ax で指定されるターゲットに対応する複数のベクトル関数が生成されます |
関連情報:
ベクトル依存関係を無視するようにコンパイラーに指示します
実際の依存関係は検出されなかったため競合検出命令を使用する必要がありません。解決方法: #pragma ivdep ディレクティブを使用して、ベクトル化が安全であることをコンパイラーに知らせます。
#pragma ivdep for (i = 0; i < n; i++) { a[index[i]] = b[i] * c; }
関連情報:
これは外部 (最も内側ではない) ループです。通常外部ループは自動ベクトル化の対象とはありません。外部ループのベクトル化は可能であり有益なこともありますが、OpenMP* API やインテル® Cilk™ Plus を使用した明示的なベクトル化が必要です。
トリップ・カウント・データを収集
サーベイレポートでは、外部ループのベクトル化の有益性を証明するトリップカウントデータが不足しています。解決方法: トリップカウント解析を実行します。
外部ループの依存関係を調査します
依存関係の有無を認識せずにベクトル化を強制するのは安全とは言えません。依存関係を調査する前に内部ループのベクトル化を無効にします。確認するには: 依存関係解析を実行します。
外部ループのメモリー・アクセス・パターンをチェックします
外部ループが適切なメモリー・アクセス・パターンを持っているか確認するには、メモリー・アクセス・パターン解析を実行します。
外部ループのベクトル化を検討してください
コンパイラーは最内ループ以外をベクトル化のターゲットとしないため、内部ループがベクトル化できれば外部ループはベクトル化されません。しかし、より適切なメモリー・アクセス・パターン、髙いトリップカウント、またはより良い依存関係構成により、外部ループのベクトル化が有益である可能性があります。
外部ループをベクトル化します。
ターゲット | ディレクティブ |
---|---|
外部ループ | #pragma omp simd |
内部ループ | #pragma novector |
#pragma omp simd for(i=0; i<N; i++) { #pragma novector for(j=0; j<N; j++) { sum += A[i]*A[j]; } }
関連情報:
外部ループのベクトル化を検討してください。
コードの複雑性がコンパイラーが判断できる基準を超えているため、コンパイラーはループをベクトル化することができませんでした。ループのベクトル化を実施できれば高いパフォーマンスを得られる可能性があります。ソースのループブロックの前で、適切なディレクティブを使用します。
ICL/ICC/ICPC ディレクティブ |
---|
#pragma omp simd |
関連情報:
外部ループのベクトル化を検討してください
依存関係の可能性が検出されたため、コンパイラーは内部ループをベクトル化できませんでした。依存関係がなければ外部ループをベクトル化します。ソースのループブロックの前で、適切なディレクティブを使用します。
ICL/ICC/ICPC ディレクティブ |
---|
#pragma omp simd |
関連情報:
STL アルゴリズムはアルゴリズム的には最適化されています。Parallel STL によって、アルゴリズム的とプログラム的の両面で最適化されたアルゴリズムを使用して、パフォーマンスを向上させます。Parallel STL は、実行ポリシーをサポートし、インテル® プロセッサー向けに最適化された、C++17 と呼ばれる次期 C++ 標準ライブラリー・アルゴリズムの実装です。アルゴリズムを呼び出す最初のパラメーターとして、次のいずれかの値を渡して実行ポリシーを指定します。
実行ポリシー | 意味 |
---|---|
seq | シーケンシャルに実行。 |
unseq | SIMD を使用。(SIMD セーフ関数が必要) |
par | マルチスレッドを使用。(スレッドセーフな関数が必要) |
par_unseq | SIMD とマルチスレッドを使用。(SIMD セーフとスレッドセーフな関数が必要) |
Parallel STL は、ランダム・アクセス・イテレーターが提供されている場合、アルゴリズムのサブセットで SIMD およびマルチスレッド実行ポリシーをサポートします。その他すべてのアルゴリズムはシーケンシャルに実行されます。
std::any_of の代わりに Parallel STL を使用する
std::any_of アルゴリズムはシーケンシャルに実行されます。並列実行するには、次の実行ポリシーのいずれかを使用して Parallel STL を使用します。std::execution::unseq
#include "pstl/execution" #include "pstl/algorithm" void foo(float* a, int n) { std::any_of(std::execution::unseq, a, a+n, [](float elem) { return elem > 100.f; }); }
関連情報:
std::copy_if の代わりに Parallel STL を使用する
std::copy_if アルゴリズムはシーケンシャルに実行されます。並列実行するには、次の実行ポリシーのいずれかを使用して Parallel STL を使用します。
#include "pstl/execution" #include "pstl/algorithm" void foo(float* a, float* b, int n) { std::copy_if(std::execution::par_unseq, a, a+n, b, [](float elem) { return elem > 10.f; }); }
関連情報:
std::for_each の代わりに Parallel STL を使用する
std::for_each アルゴリズムはシーケンシャルに実行されます。並列実行するには、次の実行ポリシーのいずれかを使用して Parallel STL を使用します。
#include "pstl/execution" #include "pstl/algorithm" void foo(float* a, int n) { std::for_each(std::execution::par_unseq, a, a+n, [](float elem) { ... }); }
関連情報:
std::sort の代わりに Parallel STL を使用する
std::any_of アルゴリズムはシーケンシャルに実行されます。並列実行するには、次の実行ポリシーのいずれかを使用して Parallel STL を使用します。std::execution::par
#include "pstl/execution" #include "pstl/algorithm" void foo(float* a, int n) { std::sort(std::execution::par, a, a+n); }
関連情報:
現在のハードウェアは、単精度と倍精度の逆数および逆数平方根命令が使用できるインテル® アドバンスト・ベクトル・エクステンション 512 (インテル® AVX-512) 命令をサポートしています。これらの命令を使用してパフォーマンスを改善します。これらの命令を使用してパフォーマンスを改善します。
可能であればベクトル化を強制します
ループには SQRT/DIV 命令 (ベクトル化の利点が考えられる) が含まれていますが、ベクトル化されていません。解決するには、以下を確認します。
関連情報:
インテル® AVX-512 ISA ターゲット
静的解析は、インテル® AVX-512 の指数および逆数 (インテル® AVX-512ER) 命令からループが利点を得られると推測しましたが、これらの命令は使用されていません。解決方法: 以下のいずれかのコンパイラー・オプションを使用します。
Windows* | Linux* |
---|---|
/QxCOMMON-AVX512 または /QaxCOMMON-AVX512 | -xCOMMON-AVX512 または -axCOMMON-AVX512 |
関連情報:
インテル® AVX-512 指数および逆数命令 ISA をターゲットにします
静的解析は、インテル® AVX-512 指数および逆数 (インテル® AVX-512ER) 命令からループが利点を得られると推測しました。この命令は現在インテル® Xeon Phi™ プロセッサーでのみサポートされていますが、このループでは使用されていませんでした。解決方法: 以下のいずれかのコンパイラー・オプションを使用します。
Windows* | Linux* |
---|---|
/QxMIC-AVX512 または /QaxMIC-AVX512 | -xMIC-AVX512 または -axMIC-AVX512 |
関連情報:
精度と浮動小数点モデルのコンパイラー・オプションを調整することで、逆数近似命令の使用を可能にします
静的解析は、逆数近似命令からループが恩恵を得られることを推測しましたが、精度と浮動小数点モデルを設定するコンパイラー・オプションがこの命令の生成を妨げている可能性があります。解決方法: 次のコンパイラー・オプションで調整します。
Windows* | Linux* | コメント |
---|---|---|
/fp | -fp-model | -fp-model=preciseは、逆数近似命令の使用を抑制します。 |
/Qimf-precision | -fimf-precision | 代わりに-fimf-precision=mediumまたは-fimf-precision=lowの利用を検討してください。 |
/Qimf-accuracy-bits | -fimf-accuracy-bits | この設定を減らすことを考慮してください。 |
/Qimf-max-error | -fimf-max-error | この設定を減らすことを考慮してください。 同様のオプションもあります。-fimf-absolute-errorディレクティブを使用します。同時に両方のオプションの使用しないでください。 |
/Qimf-absolute-error | -fimf-absolute-error | 代わりに-fimf-max-errorを使用して、-fimf-absolute-error=0に設定するか (デフォルト)、この設定を-fimf-max-errorと同時に指定して設定を増やすことを考慮してください。 |
/Qimf-domain-exclusion | -fimf-domain-exclusion | この設定を減らすことを考慮してください。クラスを除外するとコードの最適化が可能となります。注意して使用してください。アプリケーションが使用する計算がドメインから除外されていると、このオプションにより不正確な動作となる可能性があります。 |
/Qimf-arch-consistency | -fimf-arch-consistency | -fimf-arch-consistency=trueは、逆数近似命令の使用を抑制します。 |
/Qprec-div | -prec-div | -prec-divは、逆数近似命令の使用を抑制します。 |
/Qprec-sqrt | -prec-sqrt | -prec-sqrtは、逆数近似命令の使用を抑制します。 |
関連情報:
間接アドレスによるストアで、コンパイラーは潜在的な依存関係を想定しました。
これは SIMD 処理において、AVX-512 vpconflict 命令などの競合検出命令が使用され、ベクトルと生成された競合無しのサブセット内で複製された値が検出された結果によるものです。競合検出命令を排除することでパフォーマンスを改善できます。
ベクトル依存関係を無視するようにコンパイラーに指示します
実際の依存関係は検出されなかったため競合検出命令を使用する必要がありません。解決方法: #pragma ivdep ディレクティブを使用して、ベクトル化が安全であることをコンパイラーに知らせます。
#pragma ivdep for (i = 0; i < n; i++) { a[index[i]] = b[i] * c; }
関連情報:
近似操作命令を有効にしてパフォーマンスを改善します。
近似除算命令の使用を可能にします
静的解析は、ループで近似計算を使用することで利点が得られると推測しました。個々の除算は事前計算され乗算に置き換えられます。解決方法: 次のコンパイラー・オプションで調整します。
Windows* | Linux* | コメント |
---|---|---|
/Qprec-div | -no-prec-div | -no-prec-div近似除算を使用した最適化を有効にします。 |
関連情報:
近似平方根命令の使用を可能にします
静的解析は、近似平方根 (sqrt) 命令からループが恩恵を得られることを推測しましたが、精度と浮動小数点モデルを設定するコンパイラー・オプションがこの命令の生成を妨げている可能性があります。解決方法: 次のコンパイラー・オプションで調整します。
Windows* | Linux* | コメント |
---|---|---|
/Qprec-sqrt | -no-prec-sqrt | -no-prec-sqrt近似平方根最適化の使用を可能にします。 |
関連情報:
非テンポラルなストアを有効にします
#pragma vector nontemporal を使用して、非テンポラルなストアを有効にします。nontemporal 節は、特に明記されていない限り、サポートされているすべてのアーキテクチャー・ベースのシステムで、一時的ではない (ストリーミング) ストアを使用するようにコンパイラーに指示します。オプションでカンマ区切りの変数リストも指定できます。
このプラグマを指定する場合、単一または複数のスレッド内で正しいメモリーの順序設定が徹底されるように、必要なフェンスを挿入する必要があります。一般的は方法は、コンパイラーがストリーミング・ストアを使用すると思われるループ (初期化ループなど) の直後に、 _mm_sfence 組込み関数呼び出しを挿入することです。
ストリーミング・ストアは、特定のプロセッサーにおいて非ストリーミング・ストアよりも、大幅なパフォーマンスの向上をもたらす可能性があります。ただし、ストリーミング・ストアの使用を誤ると、パフォーマンスが大幅に低下します。
float a[1000]; void foo(int N){ int i; #pragma vector nontemporal for (i = 0; i < N; i++) { a[i] = 1; } }
関連情報:
メモリー内にある現在のループ配置では、CPU フロントエンドの効率が低下する可能性があります。ループコードをアライメントすることでパフォーマンスを向上します。
コンパイラーにループコードのアライメントを強制します
静的解析は、ループのコード・アライメントから利点が得られると推測しました。解決方法: より細かな制御のためコンパイラー・ディレクティブを使用して、コンパイラーにループを 2 のべき乗バイトへのアライメントを強制します。#pragma code_align (n)
内部ループを 32 バイト境界で配置します。
for (i = 0; i < n; i++) { #pragma code_align 32 for (j = 0; j < m; j++) { a[i] *= b[i] + c[j]; } }
次のコンパイラー・オプションも指定する必要があります。
Windows* | Linux* と macOS* |
---|---|
/Qalign-loops[:n] | -falign-loops[=n] |
n は、1 から 4096 の範囲内の 2 の累乗の値でなければなりません (例: 1、2、4、8、16、32、64 など)。n = 1 はアライメントを行わないことを指示します。n が省略されると、16 バイトのアライメントが使用されます。推奨: 16 と 32 を最初に試します。
/Qalign-loops-と-fno-align-loops、デフォルトのコンパイルオプションでは、特殊なループのアライメントは無効になります。