前回の記事の続きというか、補足というか、蛇足というか。
あれから
結論としてstd::mutex
使おう、と勝手に放り投げたのですが
速度的にメチャクチャ遅くて、ちょっと使い物にならないなぁという結果になったので
コメント頂いた内容など含めて、速度を検証してみました
単純な排他制御コストの比較なので、他のところで早くしろっていうツッコミは無しよ?
(例:OpenMP
のリダクション指示節とか)
※追記:std::mutex
が異常に遅いというのは、VisualStudio2013の問題だったようです。
テストプログラム
概要
int型のグローバル変数に加算処理をして、最終的な変数値を確認するという
色気も何もあったもんじゃないテストを実行して、処理時間と演算結果を確認します
排他制御については、前回の記事のコメントを参考にさせて頂いてます。
環境
Windows 7 (64bit) 上で確認
開発環境はVisualStudio2013
ターゲットはコンソール(x86)リリースビルド
CPUは4C8T
スレッド数は16、各スレッド内の処理回数は100,000回(この辺りはソースコード参照)
手法0
とりあえず基本ということで、排他制御が無い場合の時間と結果を確認するための関数を用意
void func_00()
{
g_counter++;
}
手法1
古来より伝えられし手法(CRITICAL_SECTION
を素直に使用)
void func_01()
{
EnterCriticalSection(&g_cs);
g_counter++;
LeaveCriticalSection(&g_cs);
}
手法2
std::mutexを直接叩く手法
void func_02()
{
g_mtx.lock();
g_counter++;
g_mtx.unlock();
}
手法3
前回の記事で記載したマクロを使用する手法
本質的にはCRITICAL_SECTIONを使うので、手法1と同じ
#define lock_03(x) for(char _z = (EnterCriticalSection(&x), 1); _z; _z = (LeaveCriticalSection(&x), 0))
void func_03()
{
lock_03(g_cs)
{
g_counter++;
}
}
手法4
前回の記事でコメント頂いた手法
C#のlock記述とは少し変わるが、ラムダ式に慣れている人なら素直に読めるはず
途中でreturnしてもfunc_04
からは抜けないという点において、C#のlockとは挙動が異なりますが
デッドロックする手法1よりは遥かにマシでしょう
template<typename F>
void lock_04(CRITICAL_SECTION& cs, F&& f)
{
EnterCriticalSection(&cs);
f();
LeaveCriticalSection(&cs);
}
void func_04()
{
lock_04(g_cs, []()
{
g_counter++;
});
}
手法5
前回の記事でコメント頂いた手法
critical_section オブジェクトの例外セーフ RAII ラッパーを使った方式、らしい
void func_05()
{
concurrency::critical_section::scoped_lock lock(g_cs_2);
g_counter++;
}
手法6
前回の記事でコメント頂いた手法
std::lock_guardを使用した方式
void func_06()
{
std::lock_guard<decltype(g_mtx)> lock(g_mtx);
g_counter++;
}
起動部
これらを呼び出すmainプログラムとglobal変数たち
実際にはfunc_00
からfunc_06
も同じソースコードに記述してますが
#include <cstdio>
#include <mutex>
#include <Windows.h>
#include <process.h>
#include <concrt.h>
static const int THREAD_NUM = 16;
static const int LOOP_NUM = 100000;
static int g_counter;
static CRITICAL_SECTION g_cs;
static std::mutex g_mtx;
static concurrency::critical_section g_cs_2;
unsigned int __stdcall invoker(void * param)
{
void(* func)() = (void(*)())param;
for (int i = 0; i < LOOP_NUM; i++)
func();
return 0;
}
void exec_test(void(* func)(), const char * test_name)
{
LARGE_INTEGER Clock, Time0, Time1;
HANDLE hThread[THREAD_NUM];
INT Time_ms;
/* 開始時間取得 */
QueryPerformanceFrequency(&Clock);
QueryPerformanceCounter (&Time0);
/* 変数初期化 */
g_counter = 0;
/* スレッド開始 */
for (int i = 0; i < _countof(hThread); i++)
hThread[i] = (HANDLE)_beginthreadex(NULL, 0, invoker, func, 0, NULL);
/* スレッド待機 */
WaitForMultipleObjects(_countof(hThread), hThread, TRUE, INFINITE);
/* スレッド終了 */
for (int i = 0; i < _countof(hThread); i++)
CloseHandle(hThread[i]);
/* 終了時間取得 */
QueryPerformanceCounter (&Time1);
/* 処理時間をミリ秒に変換 */
Time_ms = (1000 * (Time1.QuadPart - Time0.QuadPart) / Clock.QuadPart);
/* メッセージ出力 */
printf("%s : %6d[ms] ---- counter = %7d / %7d\n", test_name, Time_ms, g_counter, LOOP_NUM * THREAD_NUM);
}
int main(int argc, int * argv[])
{
InitializeCriticalSection(&g_cs);
timeBeginPeriod(1);
exec_test(func_00, "test_00");
exec_test(func_01, "test_01");
exec_test(func_02, "test_02");
exec_test(func_03, "test_03");
exec_test(func_04, "test_04");
exec_test(func_05, "test_05");
exec_test(func_06, "test_06");
timeEndPeriod (1);
DeleteCriticalSection(&g_cs);
}
処理結果
前置きが長くなりましたが、処理結果はコチラ
test_00 : 3[ms] ---- counter = 649817 / 1600000
test_01 : 52[ms] ---- counter = 1600000 / 1600000
test_02 : 14951[ms] ---- counter = 1600000 / 1600000
test_03 : 53[ms] ---- counter = 1600000 / 1600000
test_04 : 53[ms] ---- counter = 1600000 / 1600000
test_05 : 16136[ms] ---- counter = 1600000 / 1600000
test_06 : 17098[ms] ---- counter = 1600000 / 1600000
まず手法0については積算値が合ってないので論外ですが、トータル3msです。これが処理部だけ考えた時のCPU実力
手法1、3、4は全てCRITICAL_SECTIONを使った方式ですが、処理時間はほぼ同じです。
もうちょっとオーバーヘッドの影響でるかと思いましたが、コンパイラが賢いのかな?
手法2、5、6は他に比べて明らかに遅いです、遅すぎます。
手法2、6はstd::mutex
を使用しているので処理速度が違うのは予測が付くのですが
手法5はcritical_section
という名前の割に……内部ではstd::mutex
を使ってるような気がしますね
これ、std::mutex
を差し置いて採用する利点あるんかしら……?
最後に
やっぱり勝手にまとめますが
- 移植性を高めたいなら
std::mutex
を使おう - 中でもプログラムの堅牢性を高めたいなら、RAII原則に従ったライブラリ
std::lock_guard
等を使おう - どうしても速度が欲しいならOSのAPIを叩こう、リソース管理には細心の注意を
以上、お粗末。