【C++】Sleepの精度が低い理由とは?|timeBeginPeriodで改善する方法を解説

サムネイル画像 C++

今回は、C++での「Sleepの精度が低い理由」と「精度を改善する方法」について解説していきます。

「Sleep(1)で1ms待ちたいのに、実際は15ms以上かかってる…」
「フレームレートをSleepで制御しようとしたら、ぜんぜんズレる」
「なんでSleepって指定どおりに止まらないの?」

こんな疑問はありませんか?

Sleepは「指定した時間だけ待つ」という処理に見えますが、実はWindowsのシステムタイマーの仕組みによって、精度がかなり荒くなっています。
ここを知らないと、ゲームのフレームレート制御や処理待機で意図しない動作を引き起こしやすいです。

この記事を読み終えると、Sleepの精度問題の原因と対策がわかるようになると思いますので、ぜひ最後まで読んでいただけると嬉しいです。

そもそもSleepとは?

Sleepは、Windowsで用意されているAPIで、指定したミリ秒数だけ現在のスレッドを停止させる関数です。

使い方はシンプルで、以下のように書きます。

▼main.cpp


#include <iostream>
#include <windows.h>

int main() {
    std::cout << "待機開始\n";
    Sleep(16); // 16ミリ秒待機(のつもり)
    std::cout << "待機終了\n";
    return 0;
}

一見シンプルで便利そうですが、「16ms待って」と書いても、実際には16msぴったりでは止まりません。

なぜSleepの精度は低いのか?

原因は、WindowsのシステムタイマーのTick間隔にあります。

Windowsは一定間隔で「Tick」と呼ばれる割り込みを発生させています。SleepはこのTickのタイミングで「待機が終わったかどうか」を確認します。つまり、Tickが来るまでは目が覚めないのです。

このTickのデフォルト間隔が、約15.6msです。

つまり、Sleep(1) と書いても、次のTickが来るまで待たされるため、実際には4ms〜15ms以上かかることが普通にあります。

Sleep(1)の計測例(実際の動作)

実測値(Sleep(1) を10回計測):
約 15.6ms
約 14.2ms
約 15.6ms
約  4.8ms
約 15.6ms
...
(1msにはほど遠い)

こうなる理由は、Sleepが「ちょうど指定時間後に起こす」ではなく、「指定時間以上経過した最初のTickで起こす」仕組みだからです。

精度を改善する方法①:timeBeginPeriod

WindowsにはTickの間隔を短くするAPI、timeBeginPeriodが用意されています。
これを使うと、システムタイマーの解像度を最短約1msまで上げることができます。

▼main.cpp


#include <iostream>
#include <windows.h>
#include <timeapi.h>

#pragma comment(lib, "winmm.lib")

int main() {
    // タイマー解像度を1msに設定
    timeBeginPeriod(1);

    std::cout << "待機開始\n";
    Sleep(1);
    std::cout << "待機終了\n";

    // 必ず元に戻す
    timeEndPeriod(1);

    return 0;
}

実行結果(timeBeginPeriod使用後のSleep(1)計測)

実測値(Sleep(1) を10回計測):
約 1.5ms
約 1.6ms
約 1.8ms
約 1.5ms
約 1.9ms
...
(デフォルトより大幅に改善)

デフォルトの15ms超えに比べて、かなり安定しました。ただし、1msぴったりにはならない点は注意が必要です。

ポイントは、timeBeginPeriod(1) とセットで timeEndPeriod(1) を必ず呼ぶことです。これを忘れると、プログラムが終了するまでシステム全体のタイマー設定が変わったままになります。

精度を改善する方法②:std::chronoでビジーウェイト

より高精度に待機したい場合は、std::chrono を使ったビジーウェイトという方法もあります。
ビジーウェイトとは、時間を計測しながらループして指定時間になるまで待ち続けるやり方です。

▼main.cpp


#include <iostream>
#include <chrono>

void preciseSleep(double milliseconds) {
    auto start = std::chrono::high_resolution_clock::now();
    double ms = milliseconds;

    // 残り2ms以上あればSleepで粗く待つ(CPU負荷を抑えるため)
    while (ms > 2.0) {
        Sleep(1);
        auto now = std::chrono::high_resolution_clock::now();
        double elapsed = std::chrono::duration<double, std::milli>(now - start).count();
        ms = milliseconds - elapsed;
    }

    // 残り時間はビジーウェイトで高精度に待つ
    while (true) {
        auto now = std::chrono::high_resolution_clock::now();
        double elapsed = std::chrono::duration<double, std::milli>(now - start).count();
        if (elapsed >= milliseconds) break;
    }
}

int main() {
    std::cout << "待機開始\n";
    preciseSleep(1.0); // 約1ms待機
    std::cout << "待機終了\n";
    return 0;
}

実行結果

待機開始
待機終了
(実測:約0.98ms〜1.05ms 程度)

精度は上がりますが、ビジーウェイト中はCPUをフル回転させるので、長時間・大量に使うのはNG。短時間の高精度な待機に限定して使うのが基本です。

なお、関数分けの書き方が不安な方は、プロトタイプ宣言の記事もあわせて読むと理解しやすいです。

【重要】私が実際にゲーム制作でハマった体験談

自主制作のゲームで60fps制御を実装しようとしたとき、Sleepでまんまとハマりました。

60fpsということは、1フレームの処理時間は 1000ms ÷ 60 ≈ 16.67ms が目安です。処理にかかった時間を引いた残りをSleepで待てばいい、と思って実装したんですが、走らせると明らかにガタついて40〜50fps前後にしかなりませんでした。

原因はまさにSleepの精度。Sleep(16) と書いても、毎回15〜16msぴったりではなく、20ms以上かかるケースも頻発していたのです。
このときに timeBeginPeriod(1) を知って導入したところ、フレームレートが格段に安定しました。

「なぜかゲームがカクつく」という場合、Sleepの精度問題が原因のことは意外と多いです。

よくある失敗例と対処法


  • Sleep(1)で1msきっかり待てると思っていた

    → デフォルトでは約15.6msのTickに依存するため、Sleep(1)でも実際は数ms〜15ms以上かかります。timeBeginPeriod(1) を使うか、そもそも1ms精度が必要な設計を見直しましょう。

  • timeBeginPeriodを呼んでtimeEndPeriodを忘れた

    → timeBeginPeriodはシステム全体のタイマー設定を変更します。必ずtimeEndPeriodとペアで使いましょう。忘れるとシステムの電力消費・スケジューラに悪影響が続きます。

  • フレームレート制御をSleepだけに頼った

    → Sleepは「最低でも指定時間は待つ」保証しかありません。処理の経過時間をstd::chronoで計測しながら、残り時間をSleepで待つ設計にするとズレにくくなります。

処理の分岐を組み合わせた制御には、if文とswitch文の使い分けも参考にしてみてください。

注意点

timeBeginPeriod はWindowsのシステム全体のタイマー解像度を変更します。そのため、

  • バッテリー駆動のノートPCでは消費電力が増加する
  • システム全体のスケジューリングに影響するため、他アプリにも影響が出る場合がある
  • 使用が終わったら必ず timeEndPeriod を呼ぶ

ゲームや処理ループの開始〜終了に合わせて、スコープを意識して使うのがベストです。

まとめ

  • Sleepの精度が低い理由は、WindowsのシステムタイマーのTick間隔(デフォルト約15.6ms)に依存しているから
  • Sleep(1) と書いても、実際には4ms〜15ms以上かかることがある
  • timeBeginPeriod(1) でTick間隔を1msに縮めると精度が改善する(約1.5ms程度)
  • timeBeginPeriod必ず timeEndPeriod とセットで使う
  • より高精度が必要なら、std::chrono + ビジーウェイトを併用する
  • フレームレート制御は、経過時間計測との組み合わせが基本

関連記事