2016-03-28 10 views
0

Antony Williams' "C++ Concurrency in Action"に触発されました。std::accumulateというパラレルバージョンを詳しく見ました。私はこの本からそのコードをコピーして、デバッグの目的で、いくつかの出力を追加し、これは私がなってしまったものです:並列バージョンの累積はなぜそれほど遅いのですか?

#include <algorithm> 
#include <future> 
#include <iostream> 
#include <thread> 

template <typename Iterator, typename T> 
struct accumulate_block 
{ 
    T operator()(Iterator first, Iterator last) 
    { 
    return std::accumulate(first, last, T()); 
    } 
}; 

template <typename Iterator, typename T> 
T parallel_accumulate(Iterator first, Iterator last, T init) 
{ 
    const unsigned long length = std::distance(first, last); 

    if (!length) return init; 

    const unsigned long min_per_thread = 25; 
    const unsigned long max_threads = (length)/min_per_thread; 
    const unsigned long hardware_conc = std::thread::hardware_concurrency(); 
    const unsigned long num_threads = std::min(hardware_conc != 0 ? hardware_conc : 2, max_threads); 
    const unsigned long block_size  = length/num_threads; 

    std::vector<std::future<T>> futures(num_threads - 1); 
    std::vector<std::thread> threads(num_threads - 1); 

    Iterator block_start = first; 
    for (unsigned long i = 0; i < (num_threads - 1); ++i) 
    { 
    Iterator block_end = block_start; 
    std::advance(block_end, block_size); 

    std::packaged_task<T(Iterator, Iterator)> task{accumulate_block<Iterator, T>()}; 
    futures[i] = task.get_future(); 
    threads[i] = std::thread(std::move(task), block_start, block_end); 
    block_start = block_end; 
    } 

    T last_result = accumulate_block<Iterator, T>()(block_start, last); 

    for (auto& t : threads) t.join(); 

    T result = init; 
    for (unsigned long i = 0; i < (num_threads - 1); ++i) { 
    result += futures[i].get(); 
    } 
    result += last_result; 
    return result; 
} 

template <typename TimeT = std::chrono::microseconds> 
struct measure 
{ 
    template <typename F, typename... Args> 
    static typename TimeT::rep execution(F func, Args&&... args) 
    { 
    using namespace std::chrono; 
    auto start = system_clock::now(); 
    func(std::forward<Args>(args)...); 
    auto duration = duration_cast<TimeT>(system_clock::now() - start); 
    return duration.count(); 
    } 
}; 

template <typename T> 
T parallel(const std::vector<T>& v) 
{ 
    return parallel_accumulate(v.begin(), v.end(), 0); 
} 

template <typename T> 
T stdaccumulate(const std::vector<T>& v) 
{ 
    return std::accumulate(v.begin(), v.end(), 0); 
} 

int main() 
{ 
    constexpr unsigned int COUNT = 200000000; 
    std::vector<int> v(COUNT); 

    // optional randomising vector contents - std::accumulate also gives 0us 
    // but custom parallel accumulate gives longer times with randomised input 
    std::mt19937 mersenne_engine; 
    std::uniform_int_distribution<int> dist(1, 100); 
    auto gen = std::bind(dist, mersenne_engine); 
    std::generate(v.begin(), v.end(), gen); 
    std::fill(v.begin(), v.end(), 1); 

    auto v2 = v; // copy to work on the same data 

    std::cout << "starting ... " << '\n'; 
    std::cout << "std::accumulate : \t" << measure<>::execution(stdaccumulate<int>, v) << "us" << '\n'; 
    std::cout << "parallel: \t" << measure<>::execution(parallel<int>, v2) << "us" << '\n'; 
} 

何ここで最も興味深いのですが、ほとんどの場合、私はstd::accumulateから0の長さの時間を取得することです。

模範出力:

starting ... 
std::accumulate :  0us 
parallel: 
inside1 54us 

inside2 81830us 

inside3 89082us 
89770us 

ここでの問題は何ですか?

http://cpp.sh/6jbt

+0

一見、これは「debug my code plz」の質問とよく似ていますが、実際にここで何を尋ねていますか? –

+0

アンソニーの本に掲載されているそのバージョンがなぜそんなに遅くなっているのでしょうか?それは実際の蓄積を行うよりも並行性を管理するにはあまりにも忙しいですか? – Patryk

+0

@OliverCharlesworth彼が求めていることは、質問のタイトルに書かれています。 –

答えて

7

マイクロベンチマークといつもそうであるように、あなたはあなたのコードが実際に何かをやっていることを確認する必要があります。 accumulateを実行していますが、結果を実際にどこに格納していたり​​、何かを行ったりしていません。とにかく仕事を本当にやる必要がありますか?コンパイラは通常の場合、すべてのロジックを切り捨てました。だからこそあなたは0になります。

コードを変更して、作業を実際に行う必要があることを実際に確認してください。たとえば、

int s, s2; 
std::cout << "starting ... " << '\n'; 
std::cout << "std::accumulate : \t" 
      << measure<>::execution([&]{s = std::accumulate(v.begin(), v.end(), 0);}) 
      << "us\n"; 
std::cout << "parallel: \t" 
      << measure<>::execution([&]{s2 = parallel_accumulate(v2.begin(), v2.end(), 0);}) 
      << "us\n"; 
std::cout << s << ',' << s2 << std::endl; 
+0

あなたはまさに正しい@バリー!私のコードも変更されており、並列バージョンにはほとんど利点がありません。 (または次回は '-O3'でこのタイプのベンチマークをチェックしません:)) – Patryk

+0

@Patryk - あなたのコードの最大の問題は、モノリシックであることです。私の経験では、適切なベンチマークには複数のコンパイル単位が必要です。標準ライブラリは、関数を呼び出す関数を呼び出す関数で満たされています...しかし、適切な最適化は、関数呼び出しのない少数のアセンブリ命令に対するものを崩壊させます。適切な最適化を行ってテスト記事をコンパイルできないということは、C++を十分に活用していないことを意味します。 ... –

+0

一方、高いレベルの最適化ですべてをモノリシックにコンパイルすると、コンパイラは何もする必要がないことをよく見ています。テストドライバは、低レベルの最適化でコンパイルする必要がありますが、テストアーティクルは最適化してコンパイルする必要があります。 –

関連する問題