LoginSignup
11
9

More than 5 years have passed since last update.

非同期アレコレ

Last updated at Posted at 2016-11-14

前回の記事の続きというか、補足というか、蛇足というか。

あれから

結論としてstd::mutex使おう、と勝手に放り投げたのですが
速度的にメチャクチャ遅くて、ちょっと使い物にならないなぁという結果になったので
コメント頂いた内容など含めて、速度を検証してみました
単純な排他制御コストの比較なので、他のところで早くしろっていうツッコミは無しよ?
(例:OpenMPのリダクション指示節とか)

※追記:std::mutexが異常に遅いというのは、VisualStudio2013の問題だったようです。

テストプログラム

概要

int型のグローバル変数に加算処理をして、最終的な変数値を確認するという
色気も何もあったもんじゃないテストを実行して、処理時間と演算結果を確認します
排他制御については、前回の記事のコメントを参考にさせて頂いてます。

環境

Windows 7 (64bit) 上で確認
開発環境はVisualStudio2013
ターゲットはコンソール(x86)リリースビルド
CPUは4C8T
スレッド数は16、各スレッド内の処理回数は100,000回(この辺りはソースコード参照)

手法0

とりあえず基本ということで、排他制御が無い場合の時間と結果を確認するための関数を用意

func_00.cpp
void func_00()
{
    g_counter++;
}

手法1

古来より伝えられし手法(CRITICAL_SECTIONを素直に使用)

func_01.cpp
void func_01()
{
    EnterCriticalSection(&g_cs);

    g_counter++;

    LeaveCriticalSection(&g_cs);
}

手法2

std::mutexを直接叩く手法

func_02.cpp
void func_02()
{
    g_mtx.lock();

    g_counter++;

    g_mtx.unlock();
}

手法3

前回の記事で記載したマクロを使用する手法
本質的にはCRITICAL_SECTIONを使うので、手法1と同じ

func_03.cpp
#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よりは遥かにマシでしょう

func_04.cpp
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 ラッパーを使った方式、らしい

func_05.cpp
void func_05()
{
    concurrency::critical_section::scoped_lock lock(g_cs_2);

    g_counter++;
}

手法6

前回の記事でコメント頂いた手法
std::lock_guardを使用した方式

func_06.cpp
void func_06()
{
    std::lock_guard<decltype(g_mtx)> lock(g_mtx);

    g_counter++;
}

起動部

これらを呼び出すmainプログラムとglobal変数たち
実際にはfunc_00からfunc_06も同じソースコードに記述してますが

main.cpp
#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を叩こう、リソース管理には細心の注意を

以上、お粗末。

11
9
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
9