三浦ノート

自分の経験したことを検索可能にしていくブログ.誰かの役に立ってくれれば嬉しいです.

C++でマルチスレッド・べクトル化実行する

windows10の環境でC++のマルチスレッド・べクトル化プログラムを実行してみます。

cpuがintel core i7-3700 (4core/8thread)の環境です。

実行ポリシーを指定できる <execution> *1 が gcc9(C++17) から使えるそうなので *2 mingw-w64のgccのインストールからやりましたので本記事でまとめていきます。

目次

MSYS2,mingw-w64-gcc,gdbのインストール

MSYS2 homepageに書いてあることに従ってMSYS2のインストールとパッケージのアップデートをします。

次にMSYS2の端末上でmingw-w64のgccとgdbをインストールします。

pacman -S mingw-w64-x86_64-toolchain

と入力すると選択肢が現れますので、3と9を選びました。 f:id:OviskoutaR:20200228120854p:plain:w500

これでインストールが完了して、インストール先のパスを通せば端末上からgccが使えるようになりました。

g++ --version
g++.exe (Rev2, Built by MSYS2 project) 9.2.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

これはpowershellでの実行です。

Intel TBB のインストール

<execution>はIntel TBB(Threading Building Blocks)を使っているそうなのでこれもインストールします。 *3

pacman -S mingw-w64-x86_64-intel-tbb

をMSYS2上で実行しました。

実行例

実行環境が整いましたので実際にプログラムを実行してみます。

ここではvector<int64_t>の要素の総和をマルチスレッド化・ベクトル化して計算してみます。

以下の6つのパターンの実行時間の比較をします。マルチスレッド化は私の環境では最大で8スレッド使えます。

  1. accumulate のシングルスレッド実行
  2. future による、分割区間の accumulate マルチスレッド実行
  3. reduce のシングルスレッド実行
  4. reduce のシングルスレッド+ベクトル化実行
  5. reduce のマルチスレッド実行
  6. reduce のマルチスレッド+ベクトル化実行

以下のプログラムを実行してみました。

// accumulate.cpp
#include <iostream>
#include <future>
#include <vector>
#include <cmath>
#include <chrono>
#include <numeric>
#include <iterator>
#include <execution>
using namespace std;

const int64_t len = 100'000'000; // ベクトルサイズ
const int Nth = 8; //スレッド数
const int64_t I = len / Nth; // スレッド計算区間距離

// accumulate の分割非同期実行
template<class InputIt, class T>
T accum_multi(InputIt first, InputIt last, T init){
    vector<future<T>> f;
    for(int i = 0; i < Nth-1; ++i){ //最後より前の区間を他スレッドへ
        f.push_back(async(launch::async, accumulate<InputIt, T>, first + I * i, first + I * (i + 1), (T)0));
    }
    T sum = accumulate(first + I * (Nth-1), last, init); //最後の区間はこのスレッド

    for(future<T> &F : f){ // 他スレッドの結果を足す.
        F.wait();
        sum += F.get();
    }
    return sum;
}

int main(){
    vector<int64_t> A(len);
    iota(A.begin(), A.end(), 1); // A = {1,2,3,4,...,len}
    int64_t sum;

    //1. accumulate
    chrono::system_clock::time_point time = chrono::system_clock::now();
    sum = accumulate(A.begin(), A.end(), 0LL);
    int t = chrono::duration_cast<chrono::milliseconds>(chrono::system_clock::now() - time).count();
    cout << "1. "<<  sum << " : " << t << " msec\n";
    //2. future実装マルチスレッドaccumulate 
    time = chrono::system_clock::now();
    sum = accum_multi(A.begin(), A.end(), 0LL);
    t = chrono::duration_cast<chrono::milliseconds>(chrono::system_clock::now() - time).count();
    cout << "2. "<< sum << " : " << t << " msec\n";
    //3. reduce シングルスレッド
    time = chrono::system_clock::now();
    sum = reduce(execution::seq, A.begin(), A.end());
    t = chrono::duration_cast<chrono::milliseconds>(chrono::system_clock::now() - time).count();
    cout << "3. "<< sum << " : " << t << " msec\n";
    //4. reduce シングルスレッド+ベクトル化
    time = chrono::system_clock::now();
    sum = reduce(execution::unseq, A.begin(), A.end());
    t = chrono::duration_cast<chrono::milliseconds>(chrono::system_clock::now() - time).count();
    cout << "4. "<< sum << " : " << t << " msec\n";
    //5. reduce マルチスレッド
    time = chrono::system_clock::now();
    sum = reduce(execution::par, A.begin(), A.end());
    t = chrono::duration_cast<chrono::milliseconds>(chrono::system_clock::now() - time).count();
    cout << "5. "<< sum << " : " << t << " msec\n";
    //6. reduce マルチスレッド+ベクトル化
    time = chrono::system_clock::now();
    sum = reduce(execution::par_unseq, A.begin(), A.end());
    t = chrono::duration_cast<chrono::milliseconds>(chrono::system_clock::now() - time).count();
    cout << "6. "<< sum << " : " << t << " msec\n";
}

コンパルの際にはIntel TBBを使うために -ltbb を最後に書きます。

g++ -g -std=c++1z .\accumulate.cpp -ltbb
.\a.exe
1. 5000000050000000 : 683 msec
2. 5000000050000000 : 201 msec
3. 5000000050000000 : 1087 msec
4. 5000000050000000 : 678 msec
5. 5000000050000000 : 268 msec
6. 5000000050000000 : 164 msec

実行結果を速度が早い順に並べると。

 6. reduce のマルチスレッド+ベクトル化実行

 2. future による、分割区間の accumulate マルチスレッド実行

 5. reduce のマルチスレッド実行

 4. reduce のシングルスレッド+ベクトル化実行

 1. accumulate のシングルスレッド実行

 3. reduce のシングルスレッド実行

のようになりました。

2番目はわざわざ自分で実装した関数なのですが、それよりもSTL関数の6番目が速いというのは知れてよかったです。

gcc9以上の環境では実行ポリシーを指定できる関数を積極的に使っていきたいですね。(一応補足ですが、2番目は<future>しか使ってないので、gcc4.3(C++11)くらいから動きます。)