スパース下三角線形システムの後方解決の最適化


8

主対角線上にゼロがあるnxn下三角行列Aの圧縮スパース列(csc)表現があり、bを

(A + I)' * x = b

これは私がこれを計算するために持っているルーチンです:

void backsolve(const int*__restrict__ Lp,
               const int*__restrict__ Li,
               const double*__restrict__ Lx,
               const int n,
               double*__restrict__ x) {
  for (int i=n-1; i>=0; --i) {
      for (int j=Lp[i]; j<Lp[i+1]; ++j) {
          x[i] -= Lx[j] * x[Li[j]];
      }
  }
}

したがって、bは引数を介して渡さxれ、ソリューションによって上書きされます。LpLiLxそれぞれスパース行列の標準CSC表現の行、インデックス、及びデータポインタです。この関数はプログラムの一番上のホットスポットで、次の行があります。

x[i] -= Lx[j] * x[Li[j]];

費やされた時間の大部分である。でコンパイルgcc-8.3 -O3 -mfma -mavx -mavx512fできます

backsolve(int const*, int const*, double const*, int, double*):
        lea     eax, [rcx-1]
        movsx   r11, eax
        lea     r9, [r8+r11*8]
        test    eax, eax
        js      .L9
.L5:
        movsx   rax, DWORD PTR [rdi+r11*4]
        mov     r10d, DWORD PTR [rdi+4+r11*4]
        cmp     eax, r10d
        jge     .L6
        vmovsd  xmm0, QWORD PTR [r9]
.L7:
        movsx   rcx, DWORD PTR [rsi+rax*4]
        vmovsd  xmm1, QWORD PTR [rdx+rax*8]
        add     rax, 1
        vfnmadd231sd    xmm0, xmm1, QWORD PTR [r8+rcx*8]
        vmovsd  QWORD PTR [r9], xmm0
        cmp     r10d, eax
        jg      .L7
.L6:
        sub     r11, 1
        sub     r9, 8
        test    r11d, r11d
        jns     .L5
        ret
.L9:
        ret

vtuneによると、

vmovsd  QWORD PTR [r9], xmm0

最も遅い部分です。私は組み立ての経験がほとんどなく、この操作をさらに診断または最適化する方法について途方に暮れています。SSE、FMAなどを有効/無効にするために、さまざまなフラグを使用してコンパイルしようとしましたが、何も機能しませんでした。

プロセッサー:Xeon Skylake

質問この機能を最適化するにはどうすればよいですか?


あなたはそれを前提にすることができi >= Li[j]、すべてのためのj内部ループでは?
chqrlie

AVX512には、分散/収集命令と競合検出命令が含まれています。次の操作を実行できます。すべての負荷Li[j]がから切り離されていると仮定して、負荷を収集、ベクトル化し、i競合検出命令で仮定を確認し、すべてのが切り離されていることを確認しi、結果を計算、分散保存します。競合が検出された場合は、スカラー実装にフォールバックします。
EOF

@chqrlie残念ながらそうではありません。しかし、私たちは持っていi < Li[j] < nます。A.の下三角自然言及する質問を更新しました
user2476408を

行列はどのくらい疎ですか?追加の間接参照を使用すると逆効果になる場合があります。
chqrlie

0.1%非ゼロ要素
user2476408

回答:


2

これは、マトリックスと使用されているプラ​​ットフォームの正確なスパースパターンにかなり依存するはずです。ディメンション3006のこのマトリックスの下三角で、19554のゼロ以外のエントリを使用してgcc 8.3.0、コンパイラフラグ-O3 -march=native-march=skylakeCPUにある)でいくつかのことをテストしました。うまくいけば、これはあなたのセットアップに少し近いですが、いずれにしても、これらがどこから始めればよいのかがわかると思います。

タイミングのために、このソースファイルgoogle / benchmarkを使用しました。それは、質問で与えられた実装をベンチマークし、提案された「最適化された」実装をベンチマークするものを定義します。また、右側で完全に自明でない値を生成するために両方で使用される関数を別々にベンチマークするものもあります。「純粋な」バックソルブの時間を取得するには、かかる時間を差し引く必要があります。benchBacksolveBaselinebenchBacksolveOptimizedbenchFillRhsbenchFillRhs

1.厳密に逆方向に反復する

実装の外部ループは列を逆方向に反復し、内部ループは現在の列を順方向に反復します。同様に各列を逆方向に反復する方が一貫しているようです:

for (int i=n-1; i>=0; --i) {
    for (int j=Lp[i+1]-1; j>=Lp[i]; --j) {
        x[i] -= Lx[j] * x[Li[j]];
    }
}

これはほとんどアセンブリ(https://godbolt.org/z/CBZAT5)を変更しませんが、ベンチマークのタイミングは測定可能な改善を示しています。

------------------------------------------------------------------
Benchmark                        Time             CPU   Iterations
------------------------------------------------------------------
benchFillRhs                  2737 ns         2734 ns      5120000
benchBacksolveBaseline       17412 ns        17421 ns       829630
benchBacksolveOptimized      16046 ns        16040 ns       853333

これは何らかの形で予測可能なキャッシュアクセスが原因であると思いますが、これ以上詳しくは調べませんでした。

2.内部ループでのロード/ストアが少ない

Aは下三角であるように、我々は持っていますi < Li[j]。したがって、内側のループのx[Li[j]]変更により、これが変更さx[i]れないことがわかります。一時変数を使用して、この知識を実装に組み込むことができます。

for (int i=n-1; i>=0; --i) {
    double xi_temp = x[i];
    for (int j=Lp[i+1]-1; j>=Lp[i]; --j) {
        xi_temp -= Lx[j] * x[Li[j]];
    }
    x[i] = xi_temp;
}

これによりgcc 8.3.0、ストアが内部ループの内側からその終了直後(https://godbolt.org/z/vM4gPD)にメモリに移動します。私のシステムのテストマトリックスのベンチマークは、わずかな改善を示しています。

------------------------------------------------------------------
Benchmark                        Time             CPU   Iterations
------------------------------------------------------------------
benchFillRhs                  2737 ns         2740 ns      5120000
benchBacksolveBaseline       17410 ns        17418 ns       814545
benchBacksolveOptimized      15155 ns        15147 ns       887129

3.ループを展開します

clang最初に提案されたコードの変更後にループの展開をすでに開始していますが、gcc 8.3.0まだそうではありません。では、を追加して試してみましょう-funroll-loops

------------------------------------------------------------------
Benchmark                        Time             CPU   Iterations
------------------------------------------------------------------
benchFillRhs                  2733 ns         2734 ns      5120000
benchBacksolveBaseline       15079 ns        15081 ns       953191
benchBacksolveOptimized      14392 ns        14385 ns       963441

その実装のループも展開されるため、ベースラインも向上することに注意してください。私たちの最適化されたバージョンは、ループのアンロールからも少し恩恵を受けていますが、私たちが好んでいたほどではないかもしれません。生成されたアセンブリ(https://godbolt.org/z/_LJC5f)をgcc見ると、8つのアンロールで少し進んだようです。私のセットアップでは、実際には、1つの単純な手動アンロールで少し改善できます。フラグを-funroll-loops再度ドロップし、次のようなもので展開を実装します。

for (int i=n-1; i>=0; --i) {
    const int col_begin = Lp[i];
    const int col_end = Lp[i+1];
    const bool is_col_nnz_odd = (col_end - col_begin) & 1;
    double xi_temp = x[i];
    int j = col_end - 1;
    if (is_col_nnz_odd) {
        xi_temp -= Lx[j] * x[Li[j]];
        --j;
    }
    for (; j >= col_begin; j -= 2) {
        xi_temp -= Lx[j - 0] * x[Li[j - 0]] +
                   Lx[j - 1] * x[Li[j - 1]];
    }
    x[i] = xi_temp;
}

それで私は測定します:

------------------------------------------------------------------
Benchmark                        Time             CPU   Iterations
------------------------------------------------------------------
benchFillRhs                  2728 ns         2729 ns      5090909
benchBacksolveBaseline       17451 ns        17449 ns       822018
benchBacksolveOptimized      13440 ns        13443 ns      1018182

その他のアルゴリズム

これらのバージョンはすべて、スパースマトリックス構造に対して逆方向ソルバの同じ単純な実装を使用しています。本質的に、これらのような疎行列構造での操作は、メモリトラフィックに重大な問題を引き起こす可能性があります。少なくとも行列因数分解については、スパース構造から組み立てられた密な部分行列を操作する、より洗練された方法があります。例としては、超節点法と多正面法があります。私はこれについて少し曖昧ですが、そのような方法はこのアイデアをレイアウトにも適用し、下三角逆ソルブ(たとえば、コレスキー型分解)の密行列演算を使用すると思います。したがって、スパース構造で直接機能する単純なメソッドに固執する必要がない場合は、これらの種類のメソッドを検討することをお勧めします。この調査の例を参照してください デイビスによって。


逆方向の反復:これがより順次的なアクセスパターンにつながる場合、ハードウェアプリフェッチが役立つ可能性があります。最近のCPUはかなり良いメモリ帯域幅(特にデスクトップ/ラップトップのシングルスレッドコードの場合)、かなりひどいメモリレイテンシを持っています。したがって、L2へのHWプリフェッチは、DRAMの数百対数12サイクルのレイテンシのように巨大です。ほとんどのIntelのHWプリフェッチャーは順方向または逆方向に動作しますが、少なくとも1つは順方向にのみ動作するため、一般に、どちらかの選択がそれ以外の場合に等しい場合、ループはメモリを介して転送します。そうでない場合、後方は問題ありません。
Peter Cordes

アンロール:GCCとclangループのアンロールのもう1つの違いは、clangが(を-ffast-math使用して)複数のアキュムレータを使用することです。GCCは展開しますが、ALUレイテンシを隠すために複数の依存関係チェーンを作成する必要はありませんxi_temp -=。これにより、などの削減ループの目的のほとんどが無効になります。改善2.が予想どおりにコンパイルされた場合、ストア/リロードのストア転送レイテンシはクリティカルパスから外れますが、2倍未満のスピードで加速しますが、FPレイテンシは大きなボトルネックではないようです(代わりにメモリ/キャッシュミス)、またはそのdepチェーンが十分に短いため、OoO execは非表示になります。
Peter Cordes

1

インデックスタイプのunsigned代わりにintを使用して、数サイクルを削ることがあります>= 0

void backsolve(const unsigned * __restrict__ Lp,
               const unsigned * __restrict__ Li,
               const double * __restrict__ Lx,
               const unsigned n,
               double * __restrict__ x) {
    for (unsigned i = n; i-- > 0; ) {
        for (unsigned j = Lp[i]; j < Lp[i + 1]; ++j) {
            x[i] -= Lx[j] * x[Li[j]];
        }
    }
}

Godboltのコンパイラエクスプローラーでコンパイルすると、innerloopのコードが少し異なり、CPUパイプラインをより有効に利用できる可能性があります。テストはできませんが、試すことができます。

以下は、内部ループ用に生成されたコードです。

.L8:
        mov     rax, rcx
.L5:
        mov     ecx, DWORD PTR [r10+rax*4]
        vmovsd  xmm1, QWORD PTR [r11+rax*8]
        vfnmadd231sd    xmm0, xmm1, QWORD PTR [r8+rcx*8]
        lea     rcx, [rax+1]
        vmovsd  QWORD PTR [r9], xmm0
        cmp     rdi, rax
        jne     .L8

1
なぜこれが速くなるのか説明してもらえますか?私にとって、gcc-9.2.1は、符号拡張ロードをレジスタ幅のロードと交換することを除いて、ほとんどが実質的に同等のアセンブリを生成します。私が予測する唯一のタイミングの影響は、より悪いキャッシュへの影響です。
EOF

1
@EOF:私も同じ結論に達しました。unsigned代わりにを使用するとsize_t、キャッシュに影響を与えることなく符号拡張が回避され、コードがわずかに異なるため、パイプラインの使用が改善される可能性があります。
chqrlie

私も試しましたがunsigned、それを使ってより良いパイプラインのように見えるものは何も見ていません。私には、またはコードよりも少し悪いように見えます。とにかく、それはrex-byteを節約するとのケースでwith を使ってメモリを浪費しようとしているようです。今日はgccに少し感銘を受けていません。intsize_tgccincq %raxgcc-9.2.1 -march=skylake-avx512intunsignedincl %rax
EOF

1
@ user2476408:icc-19とclang-9.00の両方がループを展開し、反復ごとに2アイテムを処理します。
chqrlie

1
@ user2476408 icc-assemblyはまだ完全にスカラーです。ここでは何も興味深いものは見ていません。
EOF
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.