今回は、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+ ビジーウェイトを併用する - フレームレート制御は、経過時間計測との組み合わせが基本


