質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.50%
マルチスレッド

マルチスレッドは、どのように機能がコンピュータによって実行したのかを、(一般的にはスレッドとして参照される)実行の複合的な共同作用するストリームへ区分することが出来ます。

C++

C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

Q&A

解決済

3回答

1426閲覧

シングルスレッドよりマルチスレッドでのベクトル内積のほうが遅くなってしまう理由が知りたい

退会済みユーザー

退会済みユーザー

総合スコア0

マルチスレッド

マルチスレッドは、どのように機能がコンピュータによって実行したのかを、(一般的にはスレッドとして参照される)実行の複合的な共同作用するストリームへ区分することが出来ます。

C++

C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

0グッド

0クリップ

投稿2020/11/09 18:42

編集2020/11/09 18:46

質問内容

下記コードはなんの変哲もない普通の
整数同士のベクトルの内積を行う関数dotと
複数のcpuに作業を分担させて
計算を高速化させることを目論み実装された
内積関数dot_multi_threadの処理速度を
比較するためのコードです。

コードをコンパイルして実行してみましたが
dot_multi_threadでの計算が
dotよりかなり遅かったです。

こちらのサイトには

使用可能なプロセッサが複数ある場合、スレッドは使用可能なコアで同時に実行されます。

と記載されています。
なので下記コードでもdot_multi_thread関数内の
関数オブジェクトparaがスレッドの数だけ同時に動いて
計算が速くなるような気がしたのですが
結果は予想外なものでした。

なぜdot_multi_thread関数での計算の方が
遅くなってしまったのでしょうか。

どうかご回答宜しくお願いいたします。

コード

c++

1#include <iostream> 2#include <iomanip> 3#include <thread> 4#include <mutex> 5#include <chrono> 6 7// divisible_byで割り切れる最もclose_to_numに近い整数を算出 8inline size_t round_up(const size_t close_to_num, const size_t divisible_by)noexcept 9{ 10 return divisible_by == 0 ? close_to_num : close_to_num + divisible_by - (close_to_num % divisible_by); 11} 12 13// threadを用いない内積 14inline long long dot(const size_t n, const long long* const a, const long long* const b)noexcept 15{ 16 long long result = 0; 17 for(size_t i = 0; i<n; ++i) result += a[i] * b[i]; 18 return result; 19} 20 21// threadを用いる内積 22long long dot_multi_thread(const size_t n, const long long* const a, const long long* const b, const unsigned thread_num)noexcept 23{ 24 size_t npt = n / thread_num; 25 long long result = 0; 26 std::mutex mtx; 27 28 // 並列で実行する関数オブジェクト 29 auto para = [&](const size_t n, const long long* const a, const long long* const b) 30 { 31 std::lock_guard<std::mutex> lock(mtx); 32 result += dot(n, a, b); 33 }; 34 35 for(size_t i = 0; i < thread_num; ++i) 36 { 37 std::thread(para, npt, a+npt*i, b+npt*i).join(); 38 } 39 40 return result; 41} 42 43int main() 44{ 45 const unsigned thread_num = std::thread::hardware_concurrency(); 46 //const unsigned thread_num = 2;// 使用するスレッド数 47 48 size_t size = 1234; 49 size_t true_size = round_up(size, thread_num); 50 51 auto* mem1 = new long long[true_size]; 52 auto* mem2 = new long long[true_size]; 53 54 for(unsigned i = 0; i<true_size; ++i) 55 { 56 if(i < size){ 57 mem1[i] = i; 58 mem2[i] = i; 59 }else 60 { 61 mem1[i] = 0; 62 mem2[i] = 0; 63 } 64 } 65 66 auto start_1 = std::chrono::system_clock::now(); 67 auto res1 = dot(true_size, mem1, mem2); 68 auto end_1 = std::chrono::system_clock::now(); 69 double time_1 = std::chrono::duration_cast<std::chrono::nanoseconds>(end_1-start_1).count(); 70 71 auto start_2 = std::chrono::system_clock::now(); 72 auto res2 = dot_multi_thread(true_size, mem1, mem2, thread_num); 73 auto end_2 = std::chrono::system_clock::now(); 74 double time_2 = std::chrono::duration_cast<std::chrono::nanoseconds>(end_2-start_2).count(); 75 76 delete[] mem1; 77 delete[] mem2; 78 79 std::cout << "--------------------------------------------------------------" << std::endl; 80 std::cout << std::fixed << std::setprecision(20); 81 std::cout << "単一スレッド :" << res1 << std::endl; 82 std::cout << "マルチスレッド :" << res2 << std::endl; 83 84 std::cout << "--------------------------------------------------------------" << std::endl; 85 std::cout << std::fixed << std::setprecision(0); 86 std::cout << "単一スレッド :" << time_1 << "ナノ秒" << std::endl; 87 std::cout << "マルチスレッド :" << time_2 << "ナノ秒" << std::endl; 88 89 return 0; 90}

実行結果

terminal

1-------------------------------------------------------------- 2単一スレッド :625599129 3マルチスレッド :625599129 4-------------------------------------------------------------- 5単一スレッド :9901ナノ秒 6マルチスレッド :1247756ナノ秒

開発環境の備考

種類名前バージョン備考
osLinux Mint20-
cpuIntel© Core™ i7-7700 CPU @ 3.60GHz × 4-コア数4 スレッド数8
コンパイラclang++10コンパイルオプションに-pthreadを記載

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答3

0

ベストアンサー

これでかなり速くなってんじゃねぇかと。

C++

1#include <iostream> 2#include <iomanip> 3#include <thread> 4#include <mutex> 5#include <chrono> 6#include <vector> 7#include <atomic> 8 9// divisible_byで割り切れる最もclose_to_numに近い整数を算出 10inline size_t round_up(const size_t close_to_num, const size_t divisible_by)noexcept 11{ 12 return divisible_by == 0 ? close_to_num : close_to_num + divisible_by - (close_to_num % divisible_by); 13} 14 15// threadを用いない内積 16inline long long dot(const size_t n, const long long* const a, const long long* const b)noexcept 17{ 18 long long result = 0; 19 for(size_t i = 0; i<n; ++i) result += a[i] * b[i]; 20 return result; 21} 22 23// threadを用いる内積 24long long dot_multi_thread(const size_t n, const long long* const a, const long long* const b, const unsigned thread_num)noexcept 25{ 26 size_t npt = n / thread_num; 27 std::atomic<long long> result = 0; 28 29 // 並列で実行する関数オブジェクト 30 auto para = [&](const size_t n, const long long* const a, const long long* const b) 31 { 32 result += dot(n, a, b); 33 }; 34 35 std::vector<std::thread> threads; 36 37 for(size_t i = 0; i < thread_num; ++i) 38 { 39 threads.emplace_back(para, npt, a+npt*i, b+npt*i); 40 } 41 42 for ( auto& thr : threads ) { 43 thr.join(); 44 } 45 46 return result; 47} 48 49int main() 50{ 51 const unsigned thread_num = std::thread::hardware_concurrency(); 52 //const unsigned thread_num = 2;// 使用するスレッド数 53 54 size_t size = 1234; 55 size_t true_size = round_up(size, thread_num); 56 57 auto* mem1 = new long long[true_size]; 58 auto* mem2 = new long long[true_size]; 59 60 for(unsigned i = 0; i<true_size; ++i) 61 { 62 if(i < size){ 63 mem1[i] = i; 64 mem2[i] = i; 65 }else 66 { 67 mem1[i] = 0; 68 mem2[i] = 0; 69 } 70 } 71 72 auto start_1 = std::chrono::system_clock::now(); 73 auto res1 = dot(true_size, mem1, mem2); 74 auto end_1 = std::chrono::system_clock::now(); 75 double time_1 = std::chrono::duration_cast<std::chrono::nanoseconds>(end_1-start_1).count(); 76 77 auto start_2 = std::chrono::system_clock::now(); 78 auto res2 = dot_multi_thread(true_size, mem1, mem2, thread_num); 79 auto end_2 = std::chrono::system_clock::now(); 80 double time_2 = std::chrono::duration_cast<std::chrono::nanoseconds>(end_2-start_2).count(); 81 82 delete[] mem1; 83 delete[] mem2; 84 85 std::cout << "--------------------------------------------------------------" << std::endl; 86 std::cout << std::fixed << std::setprecision(20); 87 std::cout << "単一スレッド :" << res1 << std::endl; 88 std::cout << "マルチスレッド :" << res2 << std::endl; 89 90 std::cout << "--------------------------------------------------------------" << std::endl; 91 std::cout << std::fixed << std::setprecision(0); 92 std::cout << "単一スレッド :" << time_1 << "ナノ秒" << std::endl; 93 std::cout << "マルチスレッド :" << time_2 << "ナノ秒" << std::endl; 94 95 return 0; 96}

size_t size = 1234;

こいつがもっと大きくないとマルチスレッドのウマミが出ないっしょね。
※ 僕とこ(8-core)では 千倍(1234000)にしてようやくシングルスレッド≒マルチスレッド になりました

投稿2020/11/09 23:31

編集2020/11/10 02:42
episteme

総合スコア16614

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2020/11/10 08:18

epistemeさん ご回答誠に有難うございます。 確かにベクトルサイズを1234000とした私のコードと貴方のコードを 両方動かしてみましたが結果に差が出ました。 私のコード: 単一スレッド :2467573ナノ秒 マルチスレッド :3096476ナノ秒 貴方のコード: 単一スレッド :2243516ナノ秒 マルチスレッド :1185997ナノ秒 そこで疑問が出てきたのですが 両者のコードはスレッド間で共有する変数をガードするための方法が違うように見えますが std::atmicとstd::mutexは何が違うのでしょうか。 そこがわかればなぜ高速化ができたのか理解に繋がりそうです。 お返事どうかお待ちしております。
episteme

2020/11/10 08:24

> そこがわかればなぜ高速化ができたのか理解に繋がりそうです。 違います。 あなたのコードは - スレッドを起こし - そのスレッドが停止するのを待つ を繰り返しています。 よーするにマルチスレッドになっていません。同時に複数のスレッドを起こしていませんから。
退会済みユーザー

退会済みユーザー

2020/11/10 08:35

epistemeさん お返事誠にありがとうございます。 私の質問すべき内容がおかしかったです 失礼いたしました。 恐らくbjnesさんの仰っているマルチスレッドになっていないという事ですね。 bjnesさんへの返信に記載したコードをご覧になってください。 std::threadではオブジェクトが右辺値か左辺値かで挙動が 変わってしまうように見えます。 それがなぜかわかりません。 それがなぜか教えていただけましたら幸いです。 あとstd::atmicとstd::mutexについても宜しくお願いいたします。 どうか御返事お待ちしております。
episteme

2020/11/10 08:38

> std::threadではオブジェクトが右辺値か左辺値かで挙動が > 変わってしまうように見えます。 どう違うんですか?
episteme

2020/11/10 08:41

> あとstd::atmicとstd::mutexについても宜しくお願いいたします。 atomicによる排他は整数/ポインタの加減乗除程度に限定されるが、mutexより軽い。
退会済みユーザー

退会済みユーザー

2020/11/10 08:45

epistemeさん お返事誠にありがとうございます。 auto t1 = std::thread(f); auto t2 = std::thread(f); t1.join(); t2.join(); このように書けば並列に実行されている(標準出力される数列はおかしい部分がある) しかし std::thread(f).join(); std::thread(f).join(); このように書けば並列に実行されていないように見える(標準出力される数列は規則正しく並んでいる) ということです。 どうか御返事お待ちしております。
episteme

2020/11/10 08:50 編集

join() はスレッドが停止するまで「待つ(次の行に制御が移らない)」からです。 std::thread(f).join(); // [1] スレッドを起こし、それが停止するまで[2]が行われない std::thread(f).join(); // [2]
退会済みユーザー

退会済みユーザー

2020/11/10 09:09

epistemeさん お返事誠にありがとうございます。 > 待つ(次の行に制御が移らない) 私はjoin();が実行されてから初めてfが実行され始めると思っていましたがもしかして auto t1 = std::thread(f);の部分で すでにfは実行され初めており join()に移ってからfが終了するまで待つということでしょうか。 もしそうならなぜスレッドとなっていなかったのかイメージできます。 どうか御返事お待ちしております。
episteme

2020/11/10 10:14

> auto t1 = std::thread(f);の部分で > すでにfは実行され初めており > join()に移ってからfが終了するまで待つということでしょうか。 YES.
退会済みユーザー

退会済みユーザー

2020/11/10 11:38

epistemeさん 疑問が晴れてよかったです。 std::atmicとstd::mutexの違いの説明や 解釈の確認をしていただきありがとうございました。
guest

0

スレッド分割に係るマネージメントのコストというのはかなり高いです
これはシングルスレッドから見れば全く無駄な実行コストとなりますんで、たかだか数マイクロsec程度のコードをマルチスレッドにしたところで、そーゆー結果になるってことなんでしょう。

投稿2020/11/09 23:06

y_waiwai

総合スコア87719

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2020/11/10 08:19

y_waiwaiさん epistemeさんのご回答にもある通りベクトルサイズが小さすぎて 高速化できた時間よりオーバーヘッドのほうが上回った結果 こうなってしまったようですね。 ご回答誠に有難うございました。
guest

0

ななめ読みで失礼します。

並列のオーバーヘッドが問題になってそうだと感じました。

C++

1 auto para = [&](const size_t n, const long long* const a, const long long* const b) 2 { 3 std::lock_guard<std::mutex> lock(mtx); 4 result += dot(n, a, b); 5 };

std::lockguardで同期しているので
result += dot(n, a, b)が実行されるスレッド数が制限されています。

この間のスレッド待ち処理がオーバーヘッドの原因です。

並列処理させる場合はなるべく個々のスレッドが待たないような工夫が必要です。
処理の最初でmutex lockさせるようなプログラムはあまり効率が出ないと思います。

あと、join関数は処理が終わるまで待ちます。
そのため、

C++

1 for(size_t i = 0; i < thread_num; ++i) 2 { 3 std::thread(para, npt, a+npt*i, b+npt*i).join(); 4 }

はpara処理を一個ずつスレッドを立てて、そのスレッドが終わるまで待って、次のスレッドを立てて、また終わるまで待って、を繰り返す。
になります。あれ、これ、、並列処理されてなくないですか。

並列処理させる部分と同期待ちさせる部分をそれぞれ別に書くことが必要ですね。

投稿2020/11/09 23:12

bjnes

総合スコア113

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2020/11/10 08:18

bjnesさん ご回答誠に有難うございます。 #include <iostream> #include <thread> #include <vector> int main() { auto f = []() { for(unsigned i=0; i<100; i++)std::cout << i <<std::endl; }; #define PARA// コード切り替え用のスイッチ #if defined(PARA)// 並列に実行できている(数字の並びがおかしくなる) auto t1 = std::thread(f); auto t2 = std::thread(f); t1.join(); t2.join(); #else// 並列に実行できていない(数字はきれいに並んでいる) std::thread(f).join(); std::thread(f).join(); #endif return 0; } この上記コード動かしてみて 貴方の仰ると通り並列に動いていなかった事がわかりました。 しかし疑問がございます。 std::threadではオブジェクトが右辺値か左辺値かで挙動が 変わってしまうようですがなぜでしょうか。 私にはいまいちjoinの挙動がイメージできません。 どうか御返事お待ちしております。
bjnes

2020/11/10 09:05

そのまま、プログラムを、素直に読んでください。 .join();が発行された時点でそのスレッドクラスのコンストラクタに登録された処理が終わるまで、 プログラムが一時停止します。 なので、 std::thread(f).join(); std::thread(f).join(); は f実行→join()待ち→f実行→join()待ちとなります。 一方で、 auto t1 = std::thread(f); auto t2 = std::thread(f); は、fを実行してthread情報をt1に入れ、fを実行してthread情報をt2に入れる。 となります。
bjnes

2020/11/10 09:20

後、マルチスレッドプログラムにおいて、計算途中の結果を一つの場所に集積するのはあまり良くないです。 例えば16スレッドあったとして、f()を並列にする場合、 int result[16]; としてそれぞれのスレッド結果を保存する配列をつくってから、そこに結果を代入。 合計するときは、 まず8スレッドで result[threadnum] = result[threadnum] +result[threadnum+8] を実行して、 次に4スレッドで result[threadnum] = result[threadnum] +result[threadnum+4] 次に2スレッドで result[threadnum] = result[threadnum] +result[threadnum+2] 次に1スレッドで result[0] = result[0] +result[1] とすると、16回も同期待ちせずに、少ない同期待ちで高速に足し算できますよ?
退会済みユーザー

退会済みユーザー

2020/11/10 09:29

bjnesさん ご回答誠に有難うございます。 epistemeさんに同じことを質問してしまった後なのですが失礼します。 join()はコンストラクタで登録された関数の実行を始め、終了するまで待つ。 ためのメソッドかと考えていましたが std::threadクラスのコンストラクタに関数が渡された時点で処理は実行され始めており、 その処理が終了するまで待つ役割を果たすメソッドがjoinだとしたら なぜスレッドとならなかったのかイメージがつくのですが そういった解釈で正しいのでしょうか。 > 16回も同期待ちせずに、少ない同期待ちで高速に足し算できますよ? 確かに言われてみればそちらのほうが同期待ちの時間は減らせそうです。 教えてくださりありがとうございます。 どうか御返事お待ちしております。
退会済みユーザー

退会済みユーザー

2020/11/10 11:38

bjnesさん 上記の疑問点について epistemeさんよりjoinの挙動のイメージの解釈が 正しいとのご返答を頂いたの疑問は晴れました。 あと同期待ちを減らす方法についての部分が 思ってもいないことだったの今後役に立ちそうです。 ご回答誠にありがとうございました。
bjnes

2020/11/10 11:46 編集

>そういった解釈で正しいのでしょうか。 そうですね。正しいです。 あと小規模なベクトル演算ならば、CPU拡張命令を使い倒す方向に動いたほうが良きかと。 性能求めるなら、OpenBLASとかその手のライブラリを習得したほうが良きかと。 マルチスレッドプログラミングになれたいならこのまま、頑張ってください
退会済みユーザー

退会済みユーザー

2020/11/10 11:55

OpenBLASですか... 初耳なので難しそうですが調べてみます。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.50%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問