3

g++ 5.4 ( ) で自動ベクトル化を使用しようとしています-ftree-vectorize。以下のコードの配列バージョンが原因で、コンパイラーが内部ループでベクトル化の機会を逃し、ポインター バージョンと比較してパフォーマンスが大幅に異なることに気付きました。この場合、コンパイラを支援するためにできることはありますか?

void floydwarshall(float* mat, size_t n) {
#if USE_POINTER
    for (int k = 0; k < n; ++k) {
        for (int i = 0; i < n; ++i) {
            auto v = mat[i*n + k];
            for (int j = 0; j < n; ++j) {
                auto val = v + mat[k*n+j];
                if (mat[i*n + j] > val) {
                    mat[i*n + j] = val;
                }
            }
        }
    }
#else // USE_ARRAY
    typedef float (*array)[n];
    array m = reinterpret_cast<array>(mat);
    for (int k = 0; k < n; ++k) {
        for (int i = 0; i < n; ++i) {
            auto v = m[i][k];
            for (int j = 0; j < n; ++j) {
                auto val = v + m[k][j];
                if (m[i][j] > val) {
                    m[i][j] = val;
                }
            }
        }
    }
#endif
}
4

1 に答える 1

3

どちらのバージョン、g++5.4-O3 -march=haswellを使用してベクトル化し、Marc が指摘する理由により、内側のループで vcmpltps/vmaskmovps を使用します。

コンパイラが AVX 命令を使用できないようにすると、処理が難しくなります。しかし、使用するだけでは、どちらのバージョンもベクトル化されません-O3(x86-64 のベースラインであるため、SSE2 のみが利用可能です)。したがって、元の質問は、再現できない結果に基づいています。

if() を三項演算子に変更すると (コードは常に配列に格納されるため)、コンパイラはロード/MINPS/無条件に格納できます。マトリックスがキャッシュに収まらない場合、これは大量のメモリ トラフィックです。ループを別の方法でアレンジできますか?あるいはそうではないかもしれません。なぜならm[i][k]必要だからです。

更新が非常にまれで、ダーティ データの書き戻しがメモリ ボトルネックの原因になっている場合は、ベクター要素が変更されていなければ、ストアを回避するために分岐する価値さえあるかもしれません。


これは、SSE2 だけでも適切にベクトル化される配列バージョンです。入力がアラインされており、サイズが 8 の倍数 (AVX ベクトルあたりの float の数) であることをコンパイラに伝えるコードを追加しました。実際のコードでこれらの仮定を行うことができない場合は、その部分を削除してください。スカラーのイントロ/クリーンアップ コードに埋もれていないため、ベクトル化された部分を見つけやすくなります。(使用-O2 -ftree-vectorizeしても、この方法でクリーンアップ コードが完全に展開されるわけではありませんが、展開されます-O3。)

AVX がないと、gcc はまだアラインされていないロードを使用しますが、アラインされたストアを使用していることに気付きました。m[k][j]サイズが 8 の倍数である場合、サイズを揃える必要があることに気付いていないのm[i][j]でしょうか。これは、ポインター バージョンと配列バージョンの違いである可能性があります。

Godbolt コンパイラ エクスプローラーのコード

void floydwarshall_array_unconditional(float* mat, size_t n) {

    // try to tell gcc that it doesn't need scalar intro/outro code
    // The vectorized inner loop isn't particularly different without these, but it means less wading through scalar cleanup code (and less bloat if you can use this in your real code).

    // works with gcc6, doesn't work with gcc5.4
    mat = (float*)__builtin_assume_aligned(mat, 32);
    n /= 8;
    n *= 8;         // code is simpler if matrix size is always a multiple of 8 (floats per AVX vector)

    typedef float (*array)[n];
    array m = reinterpret_cast<array>(mat);

    for (size_t k = 0; k < n; ++k) {
        for (size_t i = 0; i < n; ++i) {
            auto v = m[i][k];
            for (size_t j = 0; j < n; ++j) {
                auto val = v + m[k][j];
                m[i][j] = (m[i][j]>val) ? val : m[i][j];   // Marc's suggested change: enables vectorization with unconditional stores.
            }
        }
    }
}

gcc5.4 では、ベクトル化された部分のスカラー イントロ/クリーンアップ コードを回避できませんでしたが、gcc6.2 では回避できます。ベクトル化された部分は、基本的に両方のコンパイラ バージョンで同じです。

## The inner-most loop (with gcc6.2 -march=haswell -O3)
.L5:
    vaddps  ymm0, ymm1, YMMWORD PTR [rsi+rax]
    vminps  ymm0, ymm0, YMMWORD PTR [rdx+rax]     #### Note use of minps and unconditional store, enabled by using the ternary operator instead of if().
    add     r14, 1
    vmovaps YMMWORD PTR [rdx+rax], ymm0
    add     rax, 32
    cmp     r14, r13
    jb      .L5

外部の次のループでは、(いくつかの setcc を使用して) 整数カウンターのチェックをvmovss xmm1, DWORD PTR [rax+r10*4]行い、別のvbroadcastss ymm1, xmm1. おそらく、ジャンプ先のスカラークリーンアップはブロードキャストを必要とせず、gcc は、ブロードキャスト部分が必要ない場合でも、ロードとして VBROADCASTSS を使用するだけで全体的に安価になることを知りません。

于 2016-09-16T18:23:15.567 に答える