4

私はいくつかのナイーブGEMMコードを書きましたが、なぜそれが同等のシングルスレッドGEMMコードよりもはるかに遅いのか疑問に思っています。

200x200マトリックスの場合、シングルスレッド:7ms、マルチスレッド:108ms、CPU:3930k、スレッドプールに12スレッド。

    template <unsigned M, unsigned N, unsigned P, typename T>
    static Matrix<M, P, T> multiply( const Matrix<M, N, T> &lhs, const Matrix<N, P, T> &rhs, ThreadPool & pool )
    {
        Matrix<M, P, T> result = {0};

        Task<void> task(pool);
        for (auto i=0u; i<M; ++i)
            for (auto j=0u; j<P; j++)
                task.async([&result, &lhs, &rhs, i, j](){
                    T sum = 0;
                    for (auto k=0u; k < N; ++k)
                        sum += lhs[i * N + k] * rhs[k * P + j];
                    result[i * M + j] = sum;
            });

        task.wait();

        return std::move(result);
    }
4

3 に答える 3

4

私はGEMMの経験がありませんが、あなたの問題はあらゆる種類のマルチスレッドシナリオで発生する問題に関連しているようです。

マルチスレッドを使用する場合、いくつかの潜在的なオーバーヘッドが発生しますが、その中で最も一般的なのは通常、

  1. 開始/終了スレッドの作成/クリーンアップ
  2. (スレッド数)>(CPUコア数)の場合にコンテキストスイッチ
  3. リソースのロック、ロックの取得を待機中
  4. キャッシュ同期の問題

項目2と3は、おそらくあなたの例では役割を果たしていません。12個の(ハイパースレッディング)コアで12個のスレッドを使用しており、アルゴリズムにはロックが含まれていません。

ただし、1。が関連する場合があります。合計40000のスレッドを作成し、それぞれが200の値を乗算およ​​び加算します。あまりきめの細かいスレッドを試してみることをお勧めします。おそらく最初のループの後でのみ分割します。問題を必要以上に小さく分割しないことは常に良い考えです。

また、4。あなたの場合は非常に重要です。結果を配列に書き込むときに競合状態に陥っていない間(すべてのスレッドが独自のインデックス位置に書き込んでいるため)、キャッシュ同期の大きなオーバーヘッドが発生する可能性が非常に高くなります。

"なぜ?" あなたはあなたが記憶の異なる場所に書いているので、あなたは考えるかもしれません。これは、一般的なCPUキャッシュがキャッシュラインで構成されているためです。キャッシュラインは、現在のIntelおよびAMDCPUモデルでは64バイトの長さです。これは、何かが変更されたときにキャッシュとの間の転送に使用できる最小サイズです。すべてのCPUコアが隣接するメモリワードに対して読み取りと書き込みを行っているため、4バイト(使用しているデータ型のサイズによっては8バイト)を書き込むたびに、すべてのコア間で64バイトが同期されます。

メモリが問題にならない場合は、すべての出力配列要素に「ダミー」データを「埋める」だけで、キャッシュラインごとに1つの出力要素のみが存在するようになります。4バイトのデータ型を使用している場合、これは、1つの実際のデータ要素ごとに15の配列要素をスキップすることを意味します。すべてのスレッドは、他のスレッドのメモリに干渉することなく、実質的にメモリ内の独自の連続領域にアクセスするため、スレッドの粒度を低くすると、キャッシュの問題も改善されます。

編集:ハーブサッター(C ++の教祖の1人)によるより詳細な説明はここで見つけることができます:http ://www.drdobbs.com/parallel/maximize-locality-minimize-contention/208200273

Edit2:ところで、std::movereturnステートメントでは避けることをお勧めします。これは、標準で自動的に実行されるように要求されているreturn-value-optimizationおよびcopy-elisionルールの邪魔になる可能性があるためです。複数のreturnステートメントの場合、 `std :: move`で返すのは賢明ですか?を参照してください。

于 2013-02-11T12:56:02.863 に答える
3

マルチスレッドとは、常に同期、コンテキストスイッチング、関数呼び出しを意味します。これはすべて合計され、CPUサイクルのコストがかかり、メインタスク自体に費やすことができます。

3番目のネストされたループしかない場合は、これらすべてのステップを保存し、サブルーチンの代わりにインラインで計算を実行できます。この場合、スタックをセットアップし、呼び出し、別のスレッドに切り替え、結果を返し、メインに戻す必要があります。スレッド。

マルチスレッドは、これらのコストがメインタスクと比較して小さい場合にのみ役立ちます。マトリックスが200x200より大きい場合、マルチスレッドでより良い結果が得られると思います。

于 2013-02-11T12:17:46.367 に答える
1

一般に、マルチスレッドは、デバイスへのアクセスではなく複雑さのために、多くの時間がかかるタスクに適しています。あなたが示したループは、効果的に並列化するために実行するのに時間がかかります。

スレッドの作成には多くのオーバーヘッドがあることを覚えておく必要があります。同期には、いくらかの(ただし大幅に少ない)オーバーヘッドもあります。

于 2013-02-11T13:23:37.300 に答える