更新履歴(3件・最終更新 2026年08月02日)
この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。
- 記事の冒頭に「この記事の知識マップ」節を追加しました。本文で扱っている概念とその関係を、要約・図・詳細ページへのリンクにまとめたものです。本文の主張は変えていません。
- 外部レビュー(1283件)への対応として本文を更新しました。個々の変更内容は、この下の履歴を参照してください。
- 周期のレンジ別にどこから読むかの早見表を結論の直後へ前倒しし、6章と重複していた記述を整理しました。対象読者とコード例の言語の棲み分けを冒頭に明示し、`SetProcessInformation`のC#での宣言例、用語表への追加、WPRとLatencyMonの入手方法と最小手順を加えました。既存の数値例が指標の読み方の説明用で実測値ではないことを明記し、自分の環境でp99を出す手順を追加しています。
- 初版公開
この記事を引用する(DOI: 10.5281/zenodo.21589593)
この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。
小村 豪(2026)「普通のWindowsでソフトリアルタイムをできるだけ実現するための実践ガイド」合同会社小村ソフト. https://doi.org/10.5281/zenodo.21589593 https://staging.comcomponent.com/blog/2026/03/09/000-windows-soft-realtime-practical-guide-natural/
- DOI(最新版)
- 10.5281/zenodo.21589593
- DOI(この版)
- 10.5281/zenodo.21732619
Windows で周期処理、音声処理、映像処理、計測、装置制御のような「遅れると困る」処理を作ると、「Windows では厳しいのでは」という印象を持たれがちです。この印象は半分正しく、半分は違っていて、Windows は hard real-time OS ではないものの、設計、実装、計測、運用 をきちんと詰めれば soft real-time としてかなり実用的な状態まで持っていけます。
この記事で扱うのは、特別な RTOS 拡張や独自のカーネルドライバ、専用コントローラを前提にしない、普通の Windows 10 / 11 です。普段のデスクトップ / ノート PC 上の user-mode アプリで、どこまで遅延とジッタを詰められるか という実務寄りの話になります。音声、映像、周期制御、データ取得では細部は違うものの、問題になりやすい場所はかなり共通しているので、今回はその共通部分を チェックリスト の形でまとめました。
対象読者と、コード例の言語
Windows 上で「遅れると困る」処理(周期制御、音声・映像、計測、装置制御)を作る開発者 に向けて書いています。想定しているのは user-mode のアプリ開発で、カーネルモードドライバの実装は範囲外です。
コード例の言語は、次のように分かれています。
| 内容 | 言語 | 場所 |
|---|---|---|
| 周期ループ、MMCSS、電源 QoS など Win32 API を直接叩く部分 | C++(Win32) | 4.1、4.3、4.5 |
| 同じ Win32 API を C# から呼ぶ書き方 | C#(P/Invoke) | 4.5 |
| 時刻計測、GC、割り当ての注意 | .NET(C#) | 4.4 の「.NET 側のチェック」、5.2 |
3 章までの原因の話と、4 章のチェックリスト本体は言語に依存しません。C# だけを使う方は、C++ のコードは「どの API をどの順で呼ぶか」の説明として読んでいただければ十分です。
目次
- まず結論(ひとことで)
- 1.1. 周期レンジ別の早見表(どこから読むか)
- 普通のWindowsで「ソフトリアルタイム」とは何か
- 2.1. この記事でいう「普通のWindows」
- 2.2. 何ができて、どこから難しくなるか
- 2.3. 用語を先にひとこと
- 遅延とジッタの主な原因
- 3.1. スケジューラと優先度
- 3.2. DPC / ISR とドライバ
- 3.3. ページフォルトとメモリ
- 3.4. タイマ分解能と電源管理
- 3.5. コア移動と熱
- 普通のWindowsで遅れを減らす実践チェックリスト
- 4.1. 周期ループと待機方法
- 4.2. fast path / slow path と固定長キュー
- 4.3. 優先度 / MMCSS / background mode
- 4.4. メモリ / GC / 初回コスト
- 4.5. 電源設定 / EcoQoS / timer resolution
- 4.6. CPU 配置 / コア移動 / 熱
- 4.7. ドライバ / DPC / ISR / 外乱の切り分け
- 計測と評価
- 5.1. 何を記録するか
- 5.2. p99 / p99.9 / max の見方
- 5.3. 何で見るか
- 5.4. テストの作法
- ざっくり使い分け
- まとめ
- 参考資料
この記事の知識マップ
普通のWindowsでソフトリアルタイムを狙うには、期限違反ゼロを保証するハードリアルタイムを求めず、遅延とジッタを小さくして期限を外しても壊れない設計にする。周期待機はQueryPerformanceCounterでの計測と高精度な待機可能タイマーを土台にし、必要な間だけtimeBeginPeriodでタイマー分解能を上げる。電源スロットリングでEcoQoSへの格下げとタイマー分解能の無視を明示的に解除しないと、DPC/ISR、ページフォールト、サーマルスロットリングと並んでジッタが増える一因になる。音声・映像のような連続処理ではMMCSSが優先的なCPU配分で期限超過を防ぎ、CPU配置はCPU Setsのような緩い指定を優先度の高い固定より先に試すべきで、原因の切り分けにはWPR/WPAとLatencyMonを使う。
flowchart LR
accTitle: 普通のWindowsでのソフトリアルタイムの知識マップ
accDescr: ソフトリアルタイムを支える待機可能タイマーやMMCSS、電源スロットリングとEcoQoSの関係、DPC/ISRやページフォールトがジッタを生む経路、CPU SetsからCPU固定への段階、WPR/WPAとLatencyMonによる計測手段の関係を示す図。
soft_realtime["ソフトリアルタイム"]
qpc["QPC(QueryPerformanceCounter)"]
waitable_timer["待機可能タイマー(waitable timer)"]
timebeginperiod["timeBeginPeriod(タイマー分解能要求)"]
power_throttling["Power Throttling(電力調整)"]
ecoqos["EcoQoS"]
jitter["ジッタ"]
mmcss["MMCSS(Multimedia Class Scheduler Service)"]
deadline_miss["デッドラインミス(期限違反)"]
thread_priority_class["スレッド優先度/優先度クラス"]
cpu_sets["CPU Sets"]
cpu_affinity["CPUアフィニティ"]
thermal_throttling["サーマルスロットリング"]
dpc_isr["DPC/ISR"]
page_fault["ページフォールト"]
wpr_wpa["WPR/WPA(Windows Performance Recorder/Analyzer)"]
latencymon["LatencyMon"]
soft_realtime -.->|"利用する"| qpc
soft_realtime -.->|"利用する"| waitable_timer
soft_realtime -.->|"利用する"| timebeginperiod
power_throttling -.->|"両立しない"| timebeginperiod
soft_realtime -.->|"利用する"| power_throttling
power_throttling -.->|"防止する"| ecoqos
ecoqos -.->|"原因になり得る"| jitter
soft_realtime -.->|"利用する"| mmcss
mmcss -->|"軽減する"| jitter
mmcss -->|"軽減する"| deadline_miss
thread_priority_class -.->|"原因になり得る"| jitter
thread_priority_class -->|"用いるのは非推奨"| soft_realtime
soft_realtime -.->|"利用する"| cpu_sets
cpu_sets -->|"より先に行うべき"| cpu_affinity
thermal_throttling -.->|"原因になり得る"| jitter
dpc_isr -.->|"原因になり得る"| jitter
page_fault -.->|"原因になり得る"| jitter
dpc_isr -->|"で確認できる"| wpr_wpa
dpc_isr -->|"で確認できる"| latencymon
page_fault -->|"で確認できる"| wpr_wpa
図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全20件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle
1. まず結論(ひとことで)
- 普通のWindowsで目指すのは、hard real-time の保証ではなく、soft real-time として「遅れにくく、遅れても壊れにくい」構成 です。
- 一番効果が大きいのは、ホットパスを短く・固定長に・非ブロッキングにすること です。
- fast path(取得 / 制御)と slow path(保存 / 通信 / UI)を分け、間は 固定長キュー でつなぎます。
- 周期ループは
Sleep(1)任せではなく、絶対期限 で回します。 - 音声や映像のような連続ストリームでは、まず MMCSS を検討します。
- 時間計測は QueryPerformanceCounter(QPC)、.NET なら
Stopwatchを使います。 - 待機は デバイスイベント か 高精度 waitable timer(待機可能タイマ) を優先します。
timeBeginPeriodは必要な間だけ使います。常時有効にする前提で設計しません。- 実運用では AC 給電 / 電源モード / EcoQoS の扱い / バックグラウンド負荷の整理 が効きます。
- 評価は平均値だけでなく、p99(100回測って遅い方 1 回が見え始める境目) / p99.9 / max / miss 回数 / DPC / ISR / page fault / queue 深さ で見ます。
要するに、普通の Windows では 優先度を上げることより、遅れる理由を設計で減らすこと のほうが効きます。 優先度や電源設定は重要ですが、それだけで安定性は作れません。
1.1. 周期レンジ別の早見表(どこから読むか)
長い記事なので、自分のケースに当たる行だけ読めるように 先に早見表を置きます。以前は末尾(6 章)にあった内容を、ここへ前倒ししました。
| 周期・要件 | まず組む構成 | 重点的に読む節 |
|---|---|---|
| 10〜20ms 級で、たまの揺れは吸収できる | fast path / slow path の分離、固定長キュー、通常〜やや高めの優先度、イベント駆動。これで足りることが多い | 4.1、4.2 |
| 1〜5ms 級で、継続的に間に合わせたい | 上に加えて、ホットパスの無割り当て化、専用スレッド、MMCSS または慎重な優先度調整、高精度 waitable timer、AC 給電と電源設定の見直し | 4.1 〜 4.5 |
| 1ms 未満に近づき、長時間・高負荷でも外したくない | 普通の Windows の user-mode 単独ではかなり厳しい。クリティカル部分を別の場所(デバイス側ファームウェア、専用コントローラ、FPGA、RTOS)へ逃がす設計を先に検討する | 2.2、6 |
| GUI / ログ / 通信 / DB と全部同居させたい | 「全部 1 プロセス 1 ループ」で抱え込まず、責務を分ける。後段の都合が前段の期限を壊しやすい | 4.2、4.3、6 |
原因の切り分けや計測の作法は、どのレンジでも共通です(3 章と 5 章)。
2. 普通のWindowsで「ソフトリアルタイム」とは何か
2.1. この記事でいう「普通のWindows」
ここでいう 普通の Windows は、だいたい次を前提にしています。
- Windows 10 / 11 の一般的なデスクトップ / ノート PC
- 独自の RTOS 拡張なし
- 独自のカーネルモードドライバ開発なし
- ふつうの user-mode アプリ
- 一般的な Windows API と設定で調整する
つまり、「専用機を 1 台丸ごとリアルタイム制御用に作り込む」話ではなく、「普通の Windows PC 上でどこまで現実的に詰められるか」 という話です。
flowchart LR
A["普通の Windows 10 / 11 PC"] --> B["user-mode アプリ"]
B --> C["soft real-time を目指す"]
C --> D["遅延を低くする"]
C --> E["ジッタを小さくする"]
C --> F["deadline miss を観測して壊れないようにする"]
G["期限違反ゼロを保証したい"] -.-> H["RTOS / 専用コントローラ / FPGA / デバイス側処理"]
2.2. 何ができて、どこから難しくなるか
普通の Windows でも、たとえばこのあたりの処理なら、かなり現実的に「遅れにくい」状態を作れます。
- 数ミリ秒〜数十ミリ秒の周期処理
- 音声 / 映像のバッファ駆動
- センサー取得と制御ループ
- ソフト PLC 風の一定周期処理
- UI とは別スレッドで動く低遅延パイプライン
ただし、ここでいう「できる」は たまの遅延スパイクを完全にゼロにできる という意味ではなく、狙うのはあくまでこういう状態です。
- 通常時の遅延を低くする
- ジッタを小さくする
- たまに期限を外しても壊れない
- 外した事実を観測できる
逆に、次のような要求になると、普通の Windows の user-mode だけで満たすのはかなり厳しくなります。
- 期限違反ゼロを保証したい
- 数百マイクロ秒以下を長時間安定して守りたい
- 重い GUI、ネットワーク、ストレージと同居したい
- バッテリー駆動や省電力優先のままやりたい
- ドライバやデバイス由来のスパイクも許されない
このあたりは、本当に時間に厳しい部分だけをデバイス側ファームウェア、専用コントローラ、FPGA、RTOS へ寄せる ことも考えたほうが安全です。
2.3. 用語を先にひとこと
この記事で出てくる用語の意味を、先に押さえておきます。
| 用語 | ひとことでいうと | 実務での見方 |
|---|---|---|
| soft real-time | たまの遅れはあり得るが、遅れを小さくし、遅れても壊れないようにする考え方 | 普通の Windows でまず狙うのはこれ |
| hard real-time | 期限違反ゼロを保証したい世界 | 普通の Windows の user-mode 単独で狙う対象ではない |
| ジッタ | 周期や応答時間の揺れ | 平均が良くてもジッタが大きいと実運用では不安定 |
| deadline miss | 予定時刻までに処理が終わらないこと | 隠さず、数えて、ログに出す |
| p99 / p99.9 | 遅い側のしっぽを見る指標 | p99 は「100回中、遅い方 1 回が見え始める境目」 |
| DPC / ISR | ドライバや割り込み周辺のカーネル側処理 | 長いと user-mode スレッドは待たされる |
| MMCSS | 音声 / 映像など時間に敏感な処理へ CPU を配分する Windows の仕組み | バッファを切らしたくない処理で有力 |
| QPC | QueryPerformanceCounter のこと |
経過時間計測の基本。壁時計ではなく高精度カウンタ |
| waitable timer(待機可能タイマ) | 指定時刻に signaled になるカーネルオブジェクト。CreateWaitableTimerExW に CREATE_WAITABLE_TIMER_HIGH_RESOLUTION を付けると高精度版になる |
Sleep より周期待ちの土台に向く(4.1) |
| EcoQoS | 「省電力を優先してよい」と分類された状態。CPU 周波数を下げたり、電力効率のよいコアへ寄せたりされる | 時間に敏感な処理では避ける。明示しなければ OS が自動で推測する(4.5) |
IGNORE_TIMER_RESOLUTION |
プロセスのタイマ分解能要求を無視してよい、という指定(PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION) |
これが有効だと timeBeginPeriod の効果が消える。Windows 11 では画面から隠れると自動で適用されることがある(4.5) |
| CPU Sets | スレッドやプロセスを「このコア群で動かしてほしい」と やわらかく 指定する仕組み | 特定コアへの固定より先に試す(4.6) |
| ETW / WPR / WPA | Windows 標準のトレース基盤(ETW)と、その記録ツール(WPR)・解析 GUI(WPA) | context switch、DPC / ISR、page fault を掘るときに使う(5.3) |
| LatencyMon | ドライバ起因の遅延を見るためのサードパーティ製ツール | DPC / ISR の実行時間をドライバ別に眺めて当たりを付ける(5.3) |
3. 遅延とジッタの主な原因
普通の Windows で周期処理が遅れる理由は、だいたいこの図のどれかに行き着きます。
flowchart TD
Late["周期処理が遅れる"] --> S["スケジューラ / 優先度"]
Late --> D["DPC / ISR / ドライバ"]
Late --> M["ページフォルト / メモリ"]
Late --> T["タイマ分解能 / 電源管理"]
Late --> C["コア移動 / 熱"]
3.1. スケジューラと優先度
Windows のスレッドは優先度で実行順が決まります。 同じ優先度ならラウンドロビンで回り、より高い優先度のスレッドが実行可能になると、低い優先度のスレッドは押しのけられます。
つまり、周期スレッドを真面目に書いても、
- 別スレッド
- 別プロセス
- OS の内部処理
- セキュリティ製品
- デバイス補助処理
- バックグラウンド同期
が先に走ることは普通にあります。
3.2. DPC / ISR とドライバ
ここはかなり重要です。 アプリ側の優先度を整えても、DPC(Deferred Procedure Call)や ISR(Interrupt Service Routine) が長いと、その間は user-mode のスレッドは実行できません。
原因になりやすいのは、このあたりのデバイスやドライバです。
- USB
- Wi-Fi / Bluetooth
- ストレージ
- オーディオ
- GPU
- ACPI / 電源まわり
アプリのコードが悪くなくても、ドライバやハードウェア都合で止められることがあります。 ここは「アプリの優先度をもっと上げれば勝てるだろう」と考えると、だいたい痛い目を見ます。
3.3. ページフォルトとメモリ
ホットパスで page fault(必要なページがメモリになく、取りに行くこと)が起きると、遅延が一気に大きくなります。
特に避けたいパターンを挙げておきます。
- 初回アクセスでのページコミット
- 遅延ロード
- メモリマップトファイルのページイン
- 必要以上の動的確保
- 大きなオブジェクトや断片化したヒープ
周期処理の本体では、必要なメモリを先に確保して、起動時に一度触っておく くらいでちょうどよいです。
3.4. タイマ分解能と電源管理
「1ms ごとに動かしたいから Sleep(1)」は、ほとんどの場合うまくいきません。
Windows の待機精度は、タイマ分解能、スケジューリング、電源状態の影響を受けます。
さらに、タイマ分解能を上げる設定は、待機精度を少し改善できる一方で、消費電力やシステム全体の挙動に副作用がある 点も見落とせません。
3.5. コア移動と熱
スレッドがコア間を移動すると、キャッシュの温まり直しが発生します。 これ自体は OS がうまく処理することも多いですが、負荷が高い環境では揺れの原因になります。
さらに、長時間回すと熱も無視できません。 サーマルスロットリングが入ると、それまで安定していた周期が崩れる ことがあります。
4. 普通のWindowsで遅れを減らす実践チェックリスト
ここからが実務パートです。 前の節で見た原因に対して、普通の Windows で何を確認し、何を避け、何を先に決めるべきか をチェックリスト形式でまとめます。
4.1. 周期ループと待機方法
まず典型的なアンチパターンはこれです。
while (running)
{
Sleep(1);
Step();
}
これは「1ms 周期」ではなく、だいたい 1ms 以上待ってから、その上に Step() の実行時間を足す ループです。
しかも待機オーバーシュートがそのまま累積します。
flowchart LR
subgraph Bad["相対時間ベース"]
B1["Sleep(1)"] --> B2["Step()"]
B2 --> B1
end
B2 --> B3["待機誤差と実行時間が少しずつ積み上がる"]
subgraph Good["絶対期限ベース"]
G1["next += period"] --> G2["WaitUntil(next - margin)"]
G2 --> G3["必要なら短い spin"]
G3 --> G4["FastStep()"]
G4 --> G1
end
G4 --> G5["ドリフトを溜めにくい"]
チェックリスト
Sleep(1)を周期ループの土台にしていない- 周期は
next += periodの 絶対期限 で回している - 待機は デバイスイベント か waitable timer(待機可能タイマ) を優先している
- 最後の微調整だけ、ごく短い busy-spin(空回し待機)に限定している
timeBeginPeriodは必要な間だけ使い、終わったら戻している- 最小化 / 非表示 / 見えない状態でも挙動を確認している
周期ループは、相対時間ではなく 絶対期限 で回したほうが安定します。
int64_t next = QpcNow() + periodTicks;
while (running)
{
WaitUntil(next - wakeMarginTicks);
while (QpcNow() < next)
{
CpuRelax(); // 最後だけ短く spin
}
int64_t started = QpcNow();
FastStep();
int64_t finished = QpcNow();
RecordTiming(next, started, finished);
next += periodTicks;
while (finished > next)
{
++missedDeadlines;
next += periodTicks;
}
}
4.2. fast path / slow path と固定長キュー
構成の基本は、fast path には「期限に敏感な仕事」だけを置き、それ以外は slow path へ追い出す ことです。
flowchart LR
Input["デバイス / 取得イベント"] --> Fast["fast path: 取得・制御・最小限のコピー"]
Fast --> Queue["固定長キュー"]
Queue --> Slow["slow path: 保存・送信・UI・集計"]
Fast --> Metrics["lateness / miss / queue depth を記録"]
Metrics --> Slow
fast path でやることは、このくらいに絞ります。
- データ取得
- 制御値計算
- 必要最小限のコピー
- タイムスタンプ
- キュー投入
- miss / overrun の記録
それ以外は slow path に落とします。
チェックリスト
- ホットパスでファイル書き込み、ネットワーク送信、DB 書き込みをしていない
- ホットパスで重いログ出力、
Flush、同期 RPC をしていない - fast path / slow path をスレッドや責務で明確に分けている
- キューは 固定長 にしている
- キューが溢れたときの方針を先に決めている
- miss 回数、drop 数、queue depth を観測している
- UI 更新やログ集約は低い周期へ分離している
キューが満杯になったときは、方針を曖昧にしないほうが安全です。
flowchart TD
Overflow["キューが満杯"] --> Policy{"何を守る?"}
Policy -->|最新値が大事| Latest["古い要素を捨てて最新を残す"]
Policy -->|全件が大事| All["アラート / 停止 / 上流制御"]
Policy -->|ログ用途| Log["古い要素を落として drop 数だけ記録"]
4.3. 優先度 / MMCSS / background mode
優先度の基本は、全部を上げない ことです。 普通の Windows では、「大事なスレッドだけを上げ、後ろ仕事はちゃんと下げる」ほうがうまくいきます。 background mode は、CPU だけでなく I/O などの資源も低優先度寄りに扱うための仕組みです。
flowchart TD
Work["仕事を分ける"] --> Critical["期限に敏感なスレッド"]
Work --> Worker["保存 / 送信 / 圧縮 / 集計"]
Work --> UI["UI"]
Critical --> P1["必要なら高めの優先度 or MMCSS"]
Worker --> P2["background mode / 低めの優先度"]
UI --> P3["通常優先度"]
P1 --> Warn["最初から REALTIME_PRIORITY_CLASS にはしない"]
チェックリスト
- 全スレッドを高優先度にしていない
- 本当に時間が厳しいスレッドだけを上げている
- 保存、送信、圧縮、同期などの後ろ仕事は background mode に落としている
- 音声、映像、キャプチャ、再生など連続バッファ処理では MMCSS を検討している
- プロセス全体より、まず スレッド単位 で考えている
REALTIME_PRIORITY_CLASSは、必要性が明確になるまで使わない
MMCSS(Multimedia Class Scheduler Service)は、音声 / 映像のような「一定時間内にバッファを埋めたい」処理 で特に有効です。 単純に高優先度スレッドを常時回すより、Windows の設計に沿っています。
コードの雰囲気はこんな感じです。
DWORD taskIndex = 0;
HANDLE avrt = AvSetMmThreadCharacteristicsW(L"Pro Audio", &taskIndex);
if (!avrt)
{
throw std::runtime_error("AvSetMmThreadCharacteristicsW failed");
}
// 時間に敏感なループを回す
if (!AvRevertMmThreadCharacteristics(avrt))
{
throw std::runtime_error("AvRevertMmThreadCharacteristics failed");
}
4.4. メモリ / GC / 初回コスト
ホットパスで毎回 new / malloc / List<T>.Add / 文字列連結 / LINQ を使うと、いつか回収や再配置の都合が表に出ます。GC(ガベージコレクション)自体が悪いわけではないものの、割り当ての多いコードを書けば、その影響はジッタとして表面化します。
flowchart LR
Start["起動"] --> Alloc["必要バッファを確保"]
Alloc --> Touch["一度触ってページを温める"]
Touch --> Warm["JIT / DLL 読み込み / 初回 I/O を済ませる"]
Warm --> Measure["その後に本計測 / 本運転"]
チェックリスト
- ホットパスで毎回メモリ確保 / 解放をしていない
- 必要なバッファを起動時に事前確保している
- 起動時に一度触ってページを温めている
- 初回 JIT、初回 DLL 読み込み、初回 I/O を本計測に混ぜていない
- ループ中に巨大な構造や可変長ログを育てていない
VirtualLockを使うとしても、ごく小さいクリティカル領域だけにしている
.NET 側のチェック
- 時刻計測に
Stopwatch/Stopwatch.GetTimestamp()を使っている - ホットパスで LINQ、文字列連結、
ToString()、巨大ログ生成をしていない async/awaitを hot path に持ち込んでいない- ウォームアップ前と後を分けて評価している
4.5. 電源設定 / EcoQoS / timer resolution
ここは地味ですが効きます。 コードを詰めても、上位の電源制御が強く効いていると結果は安定しません。
flowchart TD
Power["普通のWindowsの電源まわり"] --> AC["AC 給電で実行"]
Power --> Mode["電源モード: 最適なパフォーマンス寄り"]
Power --> Plan["必要なら本番用の専用電源プラン"]
Power --> QoS["時間に敏感なプロセスは EcoQoS を避ける"]
Power --> Timer["タイマ分解能要求の扱いを確認"]
チェックリスト
- 本番評価はまず AC 給電 で行っている
[設定] > [システム] > [電源 & バッテリー] > [電源モード]を 最適なパフォーマンス 寄りにしている- battery saver / 省エネ優先モードを実行中に使っていない
- ベンダー独自ユーティリティの静音 / eco / battery 優先モードを確認している
- 時間に敏感なプロセスを不用意に EcoQoS(省電力寄りの QoS)にしていない
IGNORE_TIMER_RESOLUTIONが時間に敏感なプロセス側で有効になっていない- 最小化 / 非表示時にタイマ分解能要求の効きが変わらないか確認している
- 普段使い用と、本番 / 計測 / デモ用の電源設定を分けている
timeBeginPeriod は整理して使えば役に立ちますが、万能薬ではありません。
- 必要な直前に呼ぶ
- 終わったら
timeEndPeriodで戻す - Windows 10 version 2004 以降は、昔のような完全なグローバル挙動ではない
- Windows 11 では、ウィンドウを持つプロセスが完全に隠れる / 最小化される / 見えない / 聞こえない状態だと、高い分解能が保証されないことがある
- 分解能を上げても QPC の精度が上がるわけではない
電源や QoS の影響が疑わしいなら、SetProcessInformation で power throttling の状態を確認します。
PROCESS_POWER_THROTTLING_STATE state{};
state.Version = PROCESS_POWER_THROTTLING_CURRENT_VERSION;
state.ControlMask =
PROCESS_POWER_THROTTLING_EXECUTION_SPEED |
PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION;
state.StateMask = 0; // HighQoS(性能優先寄り) + タイマ分解能要求を尊重
if (!SetProcessInformation(
GetCurrentProcess(),
ProcessPowerThrottling,
&state,
sizeof(state)))
{
throw std::runtime_error("SetProcessInformation failed");
}
ControlMask が「どの仕組みを自分で制御するか」、StateMask が「その仕組みを on にするか off にするか」です。上の例は 2 つの仕組みを制御対象に選んだうえで、どちらも off にしています。つまり EcoQoS へ落とさない(HighQoS 寄り) ことと、タイマ分解能要求を無視させない ことの宣言です。逆に ControlMask を 0 にすると、どちらも OS 任せ(既定の動作)に戻ります。
C# から呼ぶ場合
同じことを C# からやるなら、P/Invoke 宣言はこうなります。
// C# / .NET 8
using System.Runtime.InteropServices;
internal static class PowerQos
{
[StructLayout(LayoutKind.Sequential)]
private struct PROCESS_POWER_THROTTLING_STATE
{
public uint Version;
public uint ControlMask;
public uint StateMask;
}
private const uint PROCESS_POWER_THROTTLING_CURRENT_VERSION = 1;
private const uint PROCESS_POWER_THROTTLING_EXECUTION_SPEED = 0x1;
private const uint PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION = 0x4;
// PROCESS_INFORMATION_CLASS の 5 番目(0 起算で 4)が ProcessPowerThrottling
private const int ProcessPowerThrottling = 4;
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetProcessInformation(
IntPtr hProcess,
int processInformationClass,
ref PROCESS_POWER_THROTTLING_STATE processInformation,
uint processInformationSize);
[DllImport("kernel32.dll")]
private static extern IntPtr GetCurrentProcess();
/// <summary>省電力寄りの扱いと、タイマ分解能要求の無視を、どちらも解除します。</summary>
public static void OptOutOfPowerThrottling()
{
var state = new PROCESS_POWER_THROTTLING_STATE
{
Version = PROCESS_POWER_THROTTLING_CURRENT_VERSION,
ControlMask =
PROCESS_POWER_THROTTLING_EXECUTION_SPEED |
PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION,
StateMask = 0,
};
if (!SetProcessInformation(
GetCurrentProcess(),
ProcessPowerThrottling,
ref state,
(uint)Marshal.SizeOf<PROCESS_POWER_THROTTLING_STATE>()))
{
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
}
}
}
呼び出しは、時間に敏感なループを始める前に PowerQos.OptOutOfPowerThrottling(); を 1 回実行します。プロセスハンドルには PROCESS_SET_INFORMATION のアクセス権が要りますが、GetCurrentProcess() が返す自プロセスの疑似ハンドルなら問題ありません。
4.6. CPU 配置 / コア移動 / 熱
CPU 配置は、いきなり 特定コアへ固定(hard affinity / CPU pinning) するより、「なるべくこのコア群で動いてほしい」という soft affinity に近い指定 から始めるほうがうまくいくことが多いです。
flowchart LR
Measure["まず計測"] --> Ideal["SetThreadIdealProcessor / CPU Sets"]
Ideal --> Check{"十分改善した?"}
Check -->|はい| Keep["そこで止める"]
Check -->|いいえ| Hard["最後に SetThreadAffinityMask を検討"]
Measure --> Therm["同時に温度 / クロック / 長時間運転も確認"]
チェックリスト
- まず計測してから CPU 配置をいじっている
- いきなり特定コアへ固定していない
- まず
SetThreadIdealProcessorや CPU Sets を試している SetThreadAffinityMaskは最後の手段として扱っている- 長時間運転で温度、クロック、サーマルスロットリングを確認している
- ノート PC の静音モードや低騒音モードを確認している
順番としては、この流れが安全です。
- まず計測する
- 必要なら ideal processor / CPU Sets
- それでも改善が必要なら特定コア固定
特定コア固定は効きそうに見えますが、OS の逃げ道を減らす ので、安易に使うと逆に融通が利かなくなることがあります。
4.7. ドライバ / DPC / ISR / 外乱の切り分け
「たまに max だけ爆発する」「平均は良いのに p99.9 が悪い」というときは、 アプリのコード以外の外乱も疑ったほうがよいです。
flowchart TD
Spike["late / miss / max spike が出た"] --> Q1{"自分の処理時間も長い?"}
Q1 -->|はい| App["ホットパス短縮 / 割り当て削減 / I/O 除去"]
Q1 -->|いいえ| Q2{"DPC / ISR スパイクがある?"}
Q2 -->|はい| Driver["USB / Wi-Fi / Bluetooth / GPU / Audio / Storage / ACPI / ドライバ更新を確認"]
Q2 -->|いいえ| Q3{"page fault / GC / 初回コストがある?"}
Q3 -->|はい| Mem["事前確保 / ウォームアップ / ヒープ負荷削減"]
Q3 -->|いいえ| Q4{"バッテリー / 省電力 / 熱の影響がある?"}
Q4 -->|はい| Pow["AC 給電 / 電源設定 / 冷却 / 長時間テスト"]
Q4 -->|いいえ| ETW["ETW / WPA / LatencyMon で深掘り"]
チェックリスト
- Wi-Fi / Bluetooth / USB / ストレージ / GPU / オーディオまわりのドライバを確認している
- 不要なクラウド同期、インデックス作成、自動更新を止めて比較している
- 最小化したら崩れるか、画面を消したら崩れるかも試している
- LatencyMon や ETW で DPC / ISR の傾向を見ている
- 「自分の処理が重い」のか「外から止められている」のかを分けて見ている
5. 計測と評価
5.1. 何を記録するか
最低限、これだけは取っておきたいところです。
- 周期予定時刻
- 実開始時刻
- 実終了時刻
- lateness(予定開始に対してどれだけ遅れて始まったか)
- 実行時間
- missed deadline 数
- 連続 missed deadline 数
- queue depth
- drop 数
- CPU 使用率
- コア別の偏り
- DPC / ISR スパイク
- page fault
- 温度 / クロック変動
平均だけ見ても、本質はつかみにくいです。 本番で困るのは、たまに出る大きな遅延スパイクです。
5.2. p99 / p99.9 / max の見方
p99 などの指標は、遅い側のしっぽを見るためのもの です。 平均だけだと、たまに出る大きな遅延が隠れます。
| 指標 | 意味 | 10,000 回測ったときのイメージ |
|---|---|---|
| 平均 | 全体のならし値 | スパイクが埋もれやすい |
| p50 | 真ん中の値 | ふだんの体感に近い |
| p95 | 遅い方 5% が見え始める境目 | 遅い 500 回を除いた境目 |
| p99 | 遅い方 1% が見え始める境目 | 遅い 100 回を除いた境目 |
| p99.9 | 遅い方 0.1% が見え始める境目 | 遅い 10 回を除いた境目 |
| max | 最悪値 | いちばん遅かった 1 回 |
たとえば、次のような並びになったとします(指標の読み方を説明するための例で、特定の機材での実測値ではありません)。
- 平均: 0.8ms
- p99: 1.2ms
- p99.9: 3.5ms
- max: 28ms
これは、普段は速いが、たまに大きなスパイクがある 状態です。 普通の Windows では、だいたいこの p99 から max のしっぽ に本当の問題が出ます。
なお、この記事では推奨構成の効果を数値で示していません。遅延とジッタは、CPU、ドライバ、常駐ソフト、電源設定、負荷のかけ方で簡単に変わるため、他人の環境の数値はそのまま自分の環境の根拠になりません。代わりに、自分の環境で同じ形の表を作る手順を置いておきます。
自分の環境で p99 を出す最小手順
- ホットパスでは 記録だけ します。
Stopwatch.GetTimestamp()(C++ ならQueryPerformanceCounter)で lateness と実行時間を取り、事前確保した配列へ書き込みます。ここで平均やソートを計算してはいけません - 計測を止めてから集計します。ソートしてパーセンタイル位置の値を取り出します
- 同じ手順を 条件を変えて 繰り返します。ウォームアップ前後、AC / バッテリー、UI 前面 / 最小化、他プロセスの負荷あり / なし(5.4)
- 変更を 1 つ入れるたびに、同じ条件で取り直して比べます
// C# / .NET 8。集計は計測を止めたあとに行う
using System.Diagnostics;
// ホットパスでは配列へ書くだけ(割り当てゼロ)
long[] latenessTicks = new long[100_000];
int count = 0;
// 例: 周期ループの中
// latenessTicks[count++] = Stopwatch.GetTimestamp() - scheduledTimestamp;
static double PercentileMs(long[] ticks, int count, double percentile)
{
long[] sorted = ticks.AsSpan(0, count).ToArray();
Array.Sort(sorted);
int index = (int)Math.Ceiling(percentile / 100.0 * count) - 1;
index = Math.Clamp(index, 0, count - 1);
return sorted[index] * 1000.0 / Stopwatch.Frequency;
}
// 使い方
// Console.WriteLine($"p50={PercentileMs(latenessTicks, count, 50):F3}ms");
// Console.WriteLine($"p99={PercentileMs(latenessTicks, count, 99):F3}ms");
// Console.WriteLine($"p99.9={PercentileMs(latenessTicks, count, 99.9):F3}ms");
// Console.WriteLine($"max={PercentileMs(latenessTicks, count, 100):F3}ms");
Stopwatch.Frequency は 1 秒あたりのカウント数なので、割ってから 1000 を掛けるとミリ秒になります。サンプル数が少ないと p99.9 は意味を持ちません。p99.9 を語るなら、最低でも 10,000 サンプル(できれば 10 万)は集めてください。
5.3. 何で見るか
道具立てはだいたい決まっています。
- アプリ内計測
まず自前で
period / lateness / execution time / queue depth / dropを取る - ETW / WPR / WPA CPU、context switch、DPC / ISR、page fault を掘る
- LatencyMon ドライバ起因の揺れのあたりを付ける
- 温度 / クロック監視 熱の影響を見る
flowchart LR
App["アプリ内計測"] --> Dist["p50 / p95 / p99 / p99.9 / max"]
App --> Miss["miss / drop / queue depth"]
ETW["ETW / WPR / WPA"] --> Root["context switch / DPC / ISR / page fault"]
Temp["温度 / クロック監視"] --> Root
Dist --> Decide["改善の優先順位を決める"]
Miss --> Decide
Root --> Decide
WPA まで行くと少し骨が折れますが、 DPC / ISR が原因なのか、単に自分の処理が重いのか を分けるにはかなり有効です。
入手方法と、最小の使い方
| ツール | 入手方法 | 最小の手順 |
|---|---|---|
| WPR / WPA(Windows Performance Toolkit) | Windows ADK(Windows Assessment and Deployment Kit)のインストール時に「Windows Performance Toolkit」を選ぶと入ります。既定の場所は C:\Program Files (x86)\Windows Kits\10\Windows Performance Toolkit |
管理者としてコマンドプロンプトを開き、(1) wpr -start CPU で記録開始、(2) 問題の処理を数十秒動かす、(3) wpr -stop trace.etl "周期遅延の調査" で保存。あとは trace.etl を WPA で開く。使えるプロファイル名は wpr -profiles で確認できます |
| LatencyMon | Resplendence Software のサイトからダウンロードします。個人向けの Home Edition が無償版です | 起動して計測を開始し、対象の処理を動かしたまま数分放置します。カーネルタイマの最大遅延と、ドライバごとの ISR / DPC 実行時間、hard pagefault が集計されるので、実行時間が突出しているドライバを控えます |
WPA で最初に見るのは、CPU 関連のグラフのうち DPC / ISR の実行時間 と context switch です。「自分のスレッドが動けなかった時間帯に、どのドライバが CPU を握っていたか」が並ぶので、5.1 で記録した遅延の発生時刻と突き合わせます。
なお、この記事には画面のスクリーンショットを載せていません。バージョンで見た目が変わりやすいので、上の手順どおりに操作して、実際の画面で確認してください。
5.4. テストの作法
テストは、静かなベンチ環境だけでは足りません。少なくともこれらの条件は分けて見ておきたいところです。
- 起動直後のウォームアップ前
- ウォームアップ後
- 長時間連続運転
- UI 前面
- UI 最小化 / 非表示に近い状態
- AC 給電
- バッテリー駆動
- ネットワークやディスクに負荷がある状態
ベンチ環境だけで評価すると、実運用で出る問題を見落としやすくなります。 普通の Windows は「使われ方」に挙動が引っ張られやすい ので、実際に使う条件へ寄せて確認するのが大事です。
6. ざっくり使い分け
周期レンジ別の早見表は、読みながら戻れるように 1.1 へ前倒し しました。ここでは、その表だけでは決まらない 2 つの判断を補足します。
どこで「普通の Windows では無理」と判断するか
1ms 未満を長時間・高負荷で守る必要が出てきたら、チューニングを続けるより、時間に厳しい部分だけを外へ出す 判断のほうが早いことが多いです。逃がし先の候補は、デバイス側ファームウェア、専用コントローラ、FPGA、RTOS です。判断材料は主観ではなく 5 章の数字にします。
- ホットパスをこれ以上短くできないのに、p99.9 と max が要求を超え続けている
- 外乱(DPC / ISR、ドライバ、他プロセス)が原因で、アプリ側の対策では届かない(4.7 の切り分けで確認)
- 要求が「たまに外しても壊れない」ではなく「1 回も外せない」になっている
全部を 1 プロセスに同居させたいとき
GUI、ログ、通信、DB を同じプロセスの同じループで抱えると、後段の都合が前段の期限を壊します。ファイルのフラッシュ待ち、DB の再接続、UI の再描画は、いずれも数十ミリ秒単位で伸びうるからです。fast path / slow path の分離(4.2)を、スレッドではなくプロセス単位まで広げることも選択肢になります。プロセスを分けると、後段が固まっても前段の周期は回り続けます。
7. まとめ
押さえておきたい前提は 2 つあります。
- 普通の Windows で目指すのは hard real-time の保証ではなく、soft real-time として遅延とジッタを小さくし、期限違反が起きても壊れない構成にすること
- 一番効果が大きいのは、優先度調整よりホットパスの整理
実装で効くのはこのあたりです。
- fast path / slow path を分ける
- 固定長キューと、あふれたときの方針を先に決める
- QPC で測り、event / waitable timer(待機可能タイマ)で待つ
- ホットパスでは割り当て、ブロッキング I/O、重いロックを避ける
運用面ではこのあたりが効きます。
- AC 給電で動かす
- 本番用の電源設定を分ける
- 不要なバックグラウンド負荷を減らす
- p99 / p99.9 / max と miss 回数で評価する
普通の Windows でのソフトリアルタイムは、優先度設定だけで決まるものではなく、設計、実装、電源設定、計測、運用を分けて詰めていけば、かなり安定したシステムにできます。
8. 参考資料
- Multimedia Class Scheduler Service
- AvSetMmThreadCharacteristicsW function
- SetThreadPriority function
- SetPriorityClass function
- timeBeginPeriod function
- CreateWaitableTimerExW function
- Acquiring high-resolution time stamps
- QueryPerformanceCounter function
- GetSystemTimePreciseAsFileTime function
- SetProcessInformation function
- VirtualLock function
- CPU Sets
- SetThreadIdealProcessor function
- SetThreadAffinityMask function
- Processor power management options
- Change the power mode for your Windows PC
- Power settings in Windows 11
- CPU Analysis (WPA / WPT)
- WPR Command-Line Options
- Download and install the Windows ADK
- Quality of Service (EcoQoS / HighQoS)
- PROCESS_POWER_THROTTLING_STATE structure
- LatencyMon - Resplendence Software
関連する記事
同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。
業務システムのコード設計 ── 商品コード・顧客コードの決め方とチェックディジット
商品コード・顧客コードなど業務システムのコード体系を決める実践ガイド。有意コードと無意味連番の判断表、JAN・Luhn等のチェックディジット算式とC#実装、Excelの0落ち対策、桁あふれと移行まで整理します。
ADR(Architecture Decision Record)入門 ── 小規模開発で「なぜこの設計にしたか」を残す最小の方法
コードは「なぜそうしたか」を語りません。ADR(Architecture Decision Record)で設計判断の理由を1決定=1ファイルのMarkdownとして残す方法を、テンプレート、書く/書かないの判断表、実例つきで解説します。
WindowsアプリのWeb化、しない方がいいケース ── 判断表と「分割」という現実解
装置連携・ローカルファイル処理・オフライン運用を抱えたWindowsアプリでは、Web化がコスト増と機能劣化を招くことがあります。Web化に向く・向かないの判断表と、一部だけWebに出す分割構成という現実解を整理します。
WindowsでSleep(1)よりイベント待機を優先すべき理由
Windows では短い timed wait の精度は system clock の粒度とスケジューリングの影響を受けます。仕事の到着や I/O 完了、停止要求を待つなら、一定時間ごとのポーリングではなくイベント駆動にするべき理由を整理します。
想定外例外で終了すべきか継続すべきかの判断表
想定していない例外が起きたときに、アプリを終了させるべきか継続すべきかを、状態破壊・外部副作用・スレッド・ネイティブ境界の観点から整理します。
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
Windows技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
このテーマがつながるサービス
この記事は次のサービスページにつながります。近い入口からご覧ください。
Windowsアプリ開発
業務アプリ、装置連携、通信ツールなどの Windows ソフト開発を支援します。
技術相談・設計レビュー
改修方針、設計レビュー、既存資産の扱いを整理するための技術相談です。
よくある質問
この記事のテーマについて、相談時によくある質問をまとめています。
- Windowsでリアルタイム処理はできますか?
- hard real-time(期限違反ゼロ)の保証はできませんが、設計・実装・計測・運用をきちんと詰めれば、soft real-timeとしてかなり実用的な状態まで持っていけます。数ミリ秒〜数十ミリ秒の周期処理、音声/映像のバッファ駆動、センサー取得と制御ループなどは普通のWindows 10/11でも現実的です。逆に、期限違反ゼロの保証や数百マイクロ秒以下の長時間安定が必要なら、RTOS・専用コントローラ・FPGA・デバイス側処理へ寄せることを検討すべきです。
- 周期処理でSleep(1)を使ってはいけないのはなぜですか?
- Sleep(1)は「1ms周期」ではなく「だいたい1ms以上待ってから処理時間を足す」動きになり、待機オーバーシュートがそのまま累積するからです。周期ループは next += period の絶対期限で回し、待機はデバイスイベントか高精度waitable timerを優先し、最後の微調整だけをごく短いbusy-spinに限定するのが安定します。timeBeginPeriodは必要な間だけ使い、終わったら戻します。
- 遅延やジッタを減らすのに一番効くことは何ですか?
- 優先度を上げることより、ホットパスを短く・固定長に・非ブロッキングにすることです。fast path(取得・制御)とslow path(保存・通信・UI)を分けて固定長キューでつなぎ、ホットパスではファイル書き込み、ネットワーク送信、重いログ出力、毎回のメモリ確保を避けます。運用面ではAC給電、電源モードの見直し、EcoQoSの確認、バックグラウンド負荷の整理が効きます。
- 周期処理の安定性はどの指標で評価すればよいですか?
- 平均値だけでなく、p99・p99.9・max・missed deadline数で見ます。たとえば平均0.8msでもmaxが28msなら、普段は速いがたまに大きなスパイクがある状態で、普通のWindowsではこのp99からmaxのしっぽに本当の問題が出ます。あわせてDPC/ISRスパイク、page fault、queue深さ、温度・クロック変動も記録し、ウォームアップ前後・長時間運転・最小化状態・バッテリー駆動などの条件を分けて評価します。