C++
Windows

Sleepの精度をより高くできないか

概要

ミリ秒単位の短時間で定期的に処理を行いたい場合があります.
例えば, ゲームの入力を独自デバイスで取得する場合, 60Hzでは入力を取りこぼすことがあります. 単純に考えて, 描画が60FPSならば入力は120Hzで取得したいところです. また, サウンド処理では出力遅延を低くしたいものです.
 一方で, ビジーウェイトでは他スレッドへの影響や, CPUの発熱や消費電力が気になります. 消費電力や発熱なんて気にする必要がないと思われるかもしれませんが, 消費電力が減少するということは, 顧客のランニングコストが減るということです. さらに, CPUの発熱を低く抑えておくと近年の CPUにあるクロックブースト機能を利用して、突発的高負荷時のFPS安定化に繋がる可能性があります.
 このような要求において, Windowsにおける定石は以下のようにSleep(0)を使ってウェイトループを行うことです.

while(指定時間が経過していない)
    Sleep(0);

Sleepは, ドキュメントのとおり, 現在のスレッドのタイムスライスを他スレッドに渡し, 少なくとも指定時間スレッドを起動しないようにする関数です.

高精度Sleep

Sleep(0)で十分なのですが, ぎりぎりまで攻めつつ消費電力を抑える方法はないでしょうか?
端的に言いますと, PAUSE命令が使えるのではないかと.
Benefitting Power and Performance Sleep Loops
インテルプロセッサにおけるスピン・ループの使用
自分でスピンロックを作成する場合, 積極的に使用していきたい命令です.
ここで気になるのが, 他のスレッドに処理を譲るか, コアを占有しないかということです.

実験

環境は以下です.

Windows 10 Pro バージョン 1803(OSビルド 17134.112) 64bit版
Visual Studio 2017 Ver 15.5.2
CPU: Intel Core i7-6700T
Memory: 16GB (DDR4)

以下のようなプログラムで実験してみました. 3パターンの優先度で6スレッドを1コアに割り当て起動します.
時間経過チェックが何回処理されるかを計測しました, 多ければ応答性が高そうですし, 少なければ消費電力が低そうです. ただし, 全て妄想であってこれで正しい判断ができるとは思えません.

#include <Windows.h>
#include <iostream>
#include <emmintrin.h>
#include <type_traits>

#define USE_NONE (1)
//#define USE_SLEEP0 (1)
//#define USE_PAUSE (1)

struct Param
{
    __int64 count_;
    __int64 duration_;
};

DWORD WINAPI proc(LPVOID data)
{
    Param& param = *(Param*)data;
    LARGE_INTEGER startTime, currentTime;
    QueryPerformanceCounter(&startTime);
    for(;;++param.count_){
        QueryPerformanceCounter(&currentTime);
        if(param.duration_<=(currentTime.QuadPart-startTime.QuadPart)){
            break;
        }
#if defined(USE_NONE)
#elif defined(USE_SLEEP0)
        Sleep(0);
#elif defined(USE_PAUSE)
        _mm_pause();
#else
        static_assert(false);
#endif
    }
    return 0;
}

int main(int argc, char** argv)
{
    static const int Priority[] = {THREAD_PRIORITY_LOWEST, THREAD_PRIORITY_NORMAL, THREAD_PRIORITY_HIGHEST};
    timeBeginPeriod(1);
    LARGE_INTEGER frequency;
    QueryPerformanceFrequency(&frequency);

    LARGE_INTEGER time, startTime;
    QueryPerformanceCounter(&startTime);
    time.QuadPart = frequency.QuadPart*30;

    const int NumThreads = 6;
    HANDLE threads[NumThreads];
    Param params[NumThreads];
    DWORD threadIds[NumThreads];

    for(int i=0; i<NumThreads; ++i){
        threadIds[i] = i;
        params[i].count_ = 0;
        params[i].duration_ = time.QuadPart;
        threads[i] = CreateThread(NULL, 0, proc, &params[i], CREATE_SUSPENDED, &threadIds[i]);
        SetThreadAffinityMask(threads[i], 0x01U<<0);
        SetThreadPriority(threads[i], Priority[i%3]);
    }
    for(int i=0; i<NumThreads; ++i){
        ResumeThread(threads[i]);
    }

    WaitForMultipleObjects(NumThreads, threads, TRUE, INFINITE);
    timeEndPeriod(1);
    for(int i=0; i<NumThreads; ++i){
        CloseHandle(threads[i]);
        std::cout << "[" << i << "] count:" << params[i].count_ << std::endl;
    }
    return 0;
}

結果

時間経過チェックが何回処理されるかを表にしました. いずれのプログラムもWindowsタスクマネージャーでは, CPUコアの使用率は100%です.
注意していただきたいことは, この結果はシステムの状態でかなり変化するということです.

優先度 ビジーループ Sleep(0) PAUSE
THREAD_PRIORITY_LOWEST 0 506,381,605 5,679,010 4,205,188
THREAD_PRIORITY_NORMAL 0 467,229,986 4,183,812 42,403,389
THREAD_PRIORITY_HIGHEST 0 986,460,041 26,554,368 232,487,404
THREAD_PRIORITY_LOWEST 1 155,101,023 22,925,098 3,623,425
THREAD_PRIORITY_NORMAL 1 184,690,889 7,945,816 39,646,860
THREAD_PRIORITY_HIGHEST 1 1,008,522,172 26,554,251 236,099,564

まとめ

ビジーループとSleep(0)は状態に依存して結果が大きく異なるように見えます. PAUSEは比較的に状態に影響されず安定しているように見えます.
いずれの手法も何度も試してみると, システムの状況に応じてかなり変化があります, 優先度が逆転しているように見えることが普通です.

感想

正直なところ胸のもやもやが消えない状態です. いずれの手法でも, 少なくとも優先度が低いスレッドが飢餓状態にならないことはわかりました.
Inside Windows読み込めと言われればそれまでですが, ごめんなさい買ってないです.
自分のプログラムの全てのスレッドを管理できるなら, PAUSEは有用だと思いました, 〇.