行列計算などの単純で重い作業は、シングルスレッドよりもマルチスレッドで処理したほうがいいと思い、以下のコードをテストしてみました。
int main()
{
constexpr int N = 100000;
std::random_device rd;
std::mt19937 mt(rd());
std::uniform_real_distribution<double> ini(0.0, 10.0);
// single-thread
{
std::vector<int> vec(N);
for(int i = 0; i < N; ++i)
{
vec[i] = ini(mt);
}
auto start = std::chrono::system_clock::now();
for(int i = 0; i < N; ++i)
{
vec[i] = 2 * vec[i] + 3 * vec[i] - vec[i];
}
auto end = std::chrono::system_clock::now();
auto dur = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
std::cout << "single : " << dur << " ms."<< std::endl;
}
// multi-threading (Th is the number of threads)
for(int Th : {1, 2, 4, 8, 16})
{
std::vector<int> vec(N);
for(int i = 0; i < N; ++i)
{
vec[i] = ini(mt);
}
auto start = std::chrono::system_clock::now();
std::vector<std::future<void>> fut(Th);
for(int t = 0; t < Th; ++t)
{
fut[t] = std::async(std::launch::async, [t, &vec, &N, &Th]{
for(int i = t*N / Th; i < (t + 1)*N / Th; ++i)
{
vec[i] = 2 * vec[i] + 3 * vec[i] - vec[i];
}
});
}
for(int t = 0; t < Th; ++t)
{
fut[t].get();
}
auto end = std::chrono::system_clock::now();
auto dur = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
std::cout << "Th = " << Th << " : " << dur << " ms." << std::endl;
}
return 0;
}
実行環境:
OS : Windows 10 64-bit
Build-system : Visual Studio Community 2015
CPU : Core i5 4210U
このプログラムをデバッグモードでビルドすると、期待どおりの結果が得られました。
single : 146 ms.
Th = 1 : 140 ms.
Th = 2 : 71 ms.
Th = 4 : 64 ms.
Th = 8 : 61 ms.
Th = 16 : 68 ms.
これは、std::async を使用しないコードは、1 スレッドを使用するコードと同じパフォーマンスを持ち、4 または 8 スレッドを使用すると優れたパフォーマンスが得られることを示しています。
ただし、リリースモードでは、別の結果が得られました (N : 100000 -> 100000000):
single : 54 ms.
Th = 1 : 443 ms.
Th = 2 : 285 ms.
Th = 4 : 205 ms.
Th = 8 : 206 ms.
Th = 16 : 221 ms.
この結果が気になります。後半のコードだけでも、マルチスレッドの方がシングルよりもパフォーマンスが優れています。しかし、最も速いのは std::async を使用しない前半のコードです。マルチスレッドに関する最適化とオーバーヘッドがパフォーマンスに大きな影響を与えるという事実を私は知っています。でも、
- プロセスはベクトルの計算だけなので、マルチスレッド コードではなくシングルスレッド コードで最適化できるものは何ですか?
- このプログラムにはミューテックスやアトミックなどは含まれておらず、データの競合は発生しない可能性があります。マルチスレッドに関するオーバーヘッドは比較的小さいと思います。
- std::async を使用しないコードでの CPU 使用率は、マルチスレッド コードよりも小さくなります。CPUの大部分を使用するのは効率的ですか?
更新: ベクトル化について調べてみました。/Qvec-report:1
オプションを有効
//vectorized (when N is large)
for(int i = 0; i < N; ++i)
{
vec[i] = 2 * vec[i] + 3 * vec[i] - vec[i];
}
//not vectorized
auto lambda = [&vec, &N]{
for(int i = 0; i < N; ++i)
{
vec[i] = 2 * vec[i] + 3 * vec[i] - vec[i];
}
};
lambda();
//not vectorized
std::vector<std::future<void>> fut(Th);
for(int t = 0; t < Th; ++t)
{
fut[t] = std::async(std::launch::async, [t, &vec, &N, Th]{
for(int i = t*N / Th; i < (t + 1)*N / Th; ++i)
{
vec[i] = 2 * vec[i] + 3 * vec[i] - vec[i];
}
});
}
と実行時間:
single (with vectorization) : 47 ms.
single (without vectorization) : 70 ms.
マルチスレッド版では確かに for ループがベクトル化されていませんでした。ただし、その他の理由でバージョンアップに時間がかかる場合があります。
更新 2 : ラムダの for ループを書き直しました (タイプ A からタイプ B):
//Type A (the previous one)
fut[t] = std::async(std::launch::async, [t, &vec, &N, Th]{
for(int i = t*N / Th; i < (t + 1)*N / Th; ++i)
{
vec[i] = 2 * vec[i] + 3 * vec[i] - vec[i];
}
});
//Type B (the new one)
fut[t] = std::async(std::launch::async, [t, &vec, &N, Th]{
int nb = t * N / Th;
int ne = (t + 1) * N / Th;
for(int i = nb; i < ne; ++i)
{
vec[i] = 2 * vec[i] + 3 * vec[i] - vec[i];
}
});
タイプBはうまくいきました。結果 :
single (vectorized) : 44 ms.
single (invectorized) : 77 ms.
--
Th = 1 (Type A) : 435 ms.
Th = 2 (Type A) : 278 ms.
Th = 4 (Type A) : 219 ms.
Th = 8 (Type A) : 212 ms.
--
Th = 1 (Type B) : 112 ms.
Th = 2 (Type B) : 74 ms.
Th = 4 (Type B) : 60 ms.
Th = 8 (Type B) : 61 ms.
タイプ B の結果は理解できます (マルチスレッド コードは、シングルスレッドのベクトル化されたコードよりも速く実行されますが、ベクトル化されたコードほど速くはありません)。一方、タイプ A はタイプ B (一時変数を使用するだけ) と同等のように見えますが、これらは異なるパフォーマンスを示します。2 つのタイプは、異なるアセンブリ コードを生成すると見なすことができます。
更新 3 : マルチスレッド for ループの速度を低下させた要因が見つかるかもしれません。状態での分割ですfor
。これはシングルスレッドのテストです:
//ver 1 (ordinary)
fut[t] = std::async(std::launch::async, [&vec, &N]{
for(int i = 0; i < N; ++i)
{
vec[i] = 2 * vec[i] + 3 * vec[i] - vec[i];
}
});
//ver 2 (introducing a futile variable Q)
int Q = 1;
fut[t] = std::async(std::launch::async, [&vec, &N, Q]{
for(int i = 0; i < N / Q; ++i)
{
vec[i] = 2 * vec[i] + 3 * vec[i] - vec[i];
}
});
//ver 3 (using a temporary variable)
int Q = 1;
fut[t] = std::async(std::launch::async, [&vec, &N, Q]{
int end = N / Q;
for(int i = 0; i < end; ++i)
{
vec[i] = 2 * vec[i] + 3 * vec[i] - vec[i];
}
});
//ver 4 (using a raw value)
fut[t] = std::async(std::launch::async, [&vec]{
for(int i = 0; i < 100000000; ++i)
{
vec[i] = 2 * vec[i] + 3 * vec[i] - vec[i];
}
});
そして実行時間:
ver 1 : 132 ms.
ver 2 : 391 ms.
ver 3 : 47 ms.
ver 4 : 43 ms.
バージョン 3 と 4は十分に最適化されており、バージョン 1はそれほど最適化されていませんでした。これは、N がconstexpr
. 同じ理由でver2もかなり遅かったと思います。コンパイラは、N と Q が変化しないことを理解していませんでした。そのため、この条件i < N / Q
には大量のアセンブリ コードが必要となり、for ループの速度が低下しました。