42

私は新しい C++11 スレッドを試していますが、私の単純なテストではマルチコアのパフォーマンスが最悪です。簡単な例として、このプログラムは二乗乱数を加算します。

#include <iostream>
#include <thread>
#include <vector>
#include <cstdlib>
#include <chrono>
#include <cmath>

double add_single(int N) {
    double sum=0;
    for (int i = 0; i < N; ++i){
        sum+= sqrt(1.0*rand()/RAND_MAX);
    }
    return sum/N;
}

void add_multi(int N, double& result) {
    double sum=0;
    for (int i = 0; i < N; ++i){
        sum+= sqrt(1.0*rand()/RAND_MAX);
    }
    result = sum/N;
}

int main() {
    srand (time(NULL));
    int N = 1000000;

    // single-threaded
    auto t1 = std::chrono::high_resolution_clock::now();
    double result1 = add_single(N);
    auto t2 = std::chrono::high_resolution_clock::now();
    auto time_elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(t2-t1).count();
    std::cout << "time single: " << time_elapsed << std::endl;

    // multi-threaded
    std::vector<std::thread> th;
    int nr_threads = 3;
    double partual_results[] = {0,0,0};
    t1 = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < nr_threads; ++i) 
        th.push_back(std::thread(add_multi, N/nr_threads, std::ref(partual_results[i]) ));
    for(auto &a : th)
        a.join();
    double result_multicore = 0;
    for(double result:partual_results)
        result_multicore += result;
    result_multicore /= nr_threads;
    t2 = std::chrono::high_resolution_clock::now();
    time_elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(t2-t1).count();
    std::cout << "time multi: " << time_elapsed << std::endl;

    return 0;
}

Linux および 3core マシンで「g++ -std=c++11 -pthread test.cpp」を使用してコンパイルすると、典型的な結果は次のようになります。

time single: 33
time multi: 565

そのため、マルチスレッド バージョンは 1 桁以上遅くなります。乱数と sqrt を使用して例を単純化し、コンパイラの最適化を起こしやすくしたので、アイデアがありません。

編集

  1. この問題はより大きな N に対応するため、問題は実行時間が短いことではありません
  2. スレッドを作成する時間は問題ではありません。除外しても結果は大きく変わらない

うわー、私は問題を見つけました。確かに rand() でした。これを C++11 の同等のものに置き換えたところ、ランタイムが完全にスケーリングされるようになりました。みんな、ありがとう!

4

4 に答える 4

21

あなたが発見したように、randここで犯人です。

興味のある方のために説明すると、この動作は、randスレッド セーフのためにミューテックスを使用する実装に起因している可能性があります。

たとえば、eglibcは、次のように定義されているrandに関して定義します。__random

long int
__random ()
{
  int32_t retval;

  __libc_lock_lock (lock);

  (void) __random_r (&unsafe_state, &retval);

  __libc_lock_unlock (lock);

  return retval;
}

この種のロックでは、複数のスレッドが連続して実行され、パフォーマンスが低下します。

于 2013-05-23T16:10:59.423 に答える
8

プログラムの実行に必要な時間は非常に短いです (33 ミリ秒)。これは、複数のスレッドを作成して処理するためのオーバーヘッドが、実際のメリットを超える可能性があることを意味します。実行に時間がかかるプログラム (例: 10 秒) を使用してみてください。

于 2013-05-23T14:10:22.647 に答える
3

これを高速化するには、スレッド プール パターンを使用します。

std::threadこれにより、複数のスレッドを使用するたびに作成するオーバーヘッドなしで、他のスレッドでタスクをキューに入れることができます。

パフォーマンス メトリクスでキューを設定するオーバーヘッドをカウントしないでください。エンキューして結果を抽出する時間だけを考慮してください。

一連のスレッドとタスクのキュー ( を含む構造体std::function<void()>) を作成して、それらにフィードします。スレッドは、新しいタスクが実行されるのをキューで待機し、それらを実行してから、新しいタスクを待機します。

タスクは、std::future<>. 関数をタスク キューにエンキューできるコードは、このラッピング、つまり次のシグネチャを実行する場合があります。

template<typename R=void>
std::future<R> enqueue( std::function<R()> f ) {
  std::packaged_task<R()> task(f);
  std::future<R> retval = task.get_future();
  this->add_to_queue( std::move( task ) ); // if we had move semantics, could be easier
  return retval;
}

これは、裸のstd::function戻り値Rを nullary に変換しpackaged_task、それをタスク キューに追加します。タスク キューは移動専用であるため、移動対応である必要があることに注意してくださいpackaged_task

注 1: 私は にあまり詳しくないstd::futureので、上記は間違っている可能性があります。

注 2: 上記のキューに入れられたタスクが中間結果に関して相互に依存している場合、ブロックされたスレッドを「再利用」して新しいコードを実行するための規定が記述されていないため、キューがデッドロックする可能性があります。ただし、「裸の計算」ノンブロッキング タスクは、上記のモデルで正常に動作するはずです。

于 2013-05-23T14:22:11.313 に答える