Windows 軟即時實戰指南 - 為了減少延遲的檢查清單

· · Windows 開發, 軟即時, 設計, 量測

這篇文章要談的不是加了特殊即時擴充的 Windows,而是 一般的 Windows 10 / 11。 對象是,在普通桌機或筆電上執行的 user-mode 一般應用程式

這裡要追求的不是 hard real-time 的保證。 即使是普通的 Windows,只要把設計、等待方式、優先順序、電源設定、量測整合起來,就能把 soft real-time 做到相當實用的狀態

這次不以完整列出所有項目為目標,而是做成 能立刻掌握「該重看什麼」的結構。 先看第 4 節的檢查清單,大概就能看出要重看哪裡。

1. 先說結論(一句話)

  • 在普通 Windows 上,要追求的不是 hard real-time 的保證,而是縮小延遲與抖動,並減少 deadline miss
  • 首先要重看的不是優先順序的值,而是 週期執行緒裡到底塞了什麼
  • 每週期必經、對時間敏感的處理放進 fast path,儲存、通訊、UI 這類可以稍晚的處理歸到 slow path
  • 在 fast path 中避免依賴 Sleep 的等待、阻塞 I/O、每次的配置/釋放、以及無上限的佇列
  • 音訊或影片這類持續串流,首先考慮 MMCSS
  • 實際營運中,AC 供電、電源模式、timer resolution、power throttling、背景負載 會有影響
  • 評估不要只看平均值,還要看 p99 / p99.9 / max / miss 次數 / queue 深度 / DPC / ISR / page fault

實務上大致按以下順序:

  1. 把週期迴圈從依賴 Sleep 上拿下來
  2. 把 fast path 與 slow path 切開
  3. 把佇列改成固定長度,並先決定溢位時的方針
  4. 把 I/O、配置、重鎖從 fast path 移出
  5. 只讓必要的執行緒用優先順序或 MMCSS
  6. 把普通 Windows 的電源設定與量測整合到位

想掌握「在普通 Windows 上,改哪裡才比較不會延遲」,依這個順序看會最清楚。

2. 這篇文章講的「普通 Windows」是什麼

這裡說的「普通 Windows」指的是 Windows 10 / 11 的一般 PC。 不使用特殊的即時擴充或專用 OS,只在 Windows 標準 API 與設定的範圍內,看能把穩定性推到什麼程度,這是本文的立場。

flowchart TB
    scope["本文的對象"] --> s1["Windows 10 / 11 的一般 PC"]
    scope --> s2["user-mode 的一般應用程式"]
    scope --> s3["以標準 API 與標準設定為主"]
    scope --> s4["普通桌機 / 筆電"]

    out["本文的對象外"] --> o1["專用 RTOS 或 RT 擴充"]
    out --> o2["以核心驅動為主的控制"]
    out --> o3["把嚴苛時間需求全面移交給 FPGA 或 MCU 的構成"]

2.1. 涵蓋的範圍

涵蓋的例如:

  • 執行在 Windows 10 / 11 上的 C++ / C# user-mode 應用程式
  • 音訊、影像、量測、裝置控制、週期處理等需要「延遲小」的軟體
  • 一般桌機、工作站、筆電

也就是說,討論的是「作為能正常部署的 Windows 應用程式,能撐到多遠」。

2.2. 排除在外的

另一方面,以下在本文的主線之外:

  • hard real-time 的保證
  • 導入專用 RTOS 或即時擴充產品
  • 以核心驅動或自製驅動為主角的設計
  • 把時間敏感部分全面移到 FPGA、MCU、專用控制器

當需求更嚴苛時,這些選擇確實必要。 不過在一開始就往那邊走之前,先在普通 Windows 上累積可重現的改善 仍然相當有價值。

3. Windows 上的軟即時是什麼

3.1. 目標

在普通 Windows 上要達到的狀態如下:

  • 把平時的延遲壓低
  • 讓抖動變小
  • 偶爾出現延遲尖峰也不會壞
  • 能觀測到「沒趕上期限」的次數

這裡說的 soft real-time,不是「絕不遲到」,而是 不易遲到、遲到能被看到、就算遲到也不會壞 的思路。

3.2. 本文使用的名詞

先釐清這 3 個,讀起來會順很多。

名詞 意思
延遲 處理比預計時間晚開始,或晚結束
抖動 週期或處理時間的波動。無法每次都以同樣間隔運作
deadline miss 在既定期限內沒能完成處理

例如想每 1ms 動一次的處理,在 1.8ms 後才開始,那這 0.8ms 就是延遲。 如果不是每次都 1.0ms,而是 0.9ms、1.3ms、2.1ms 這樣搖擺,那就是抖動。

3.3. 什麼情況會開始變難

只靠普通 Windows 的 user-mode 應用程式,會開始吃緊的需求例如:

  • 要保證零期限違反
  • 要長時間穩定守住數百微秒以下
  • 要把沉重 GUI、通訊、儲存與高頻週期處理全部擠在一起
  • 用電池驅動或省電優先時也要守住嚴格週期
  • 連來自驅動或裝置的尖峰也不被允許

到了這種程度,普通 Windows 單獨就會吃緊。 這時候最好考慮 只把真正對時間嚴苛的部分交給韌體、專用控制器、FPGA、另一個 RTOS

4. 先看的檢查清單

本節是 為了先掌握「該注意哪些」而設的。 先看下面這張表,大致就能抓到要重看的位置。

4.1. 全貌

要確認的項目 首先要做 典型的 NG
等待方式 以絕對期限運作。使用事件驅動或高精度 waitable timer Sleep(1) 為基礎的週期迴圈
處理分離 切開 fast path 與 slow path 把儲存、送信、UI 放進 fast path
佇列 固定長度,先定好溢位方針 無上限佇列把問題後拖
fast path 內容 去掉配置、重日誌、阻塞 I/O、重鎖 new / malloc / 同步 I/O / 每次重建字串
優先順序 只提高必要的執行緒;音訊/影像考慮 MMCSS 一口氣開 REALTIME_PRIORITY_CLASS
OS / 電源 確認 AC 供電、電源模式、timer resolution、power throttling 以電池驅動或省電模式做評估
量測 取 lateness、執行時間、miss 次數、queue 深度 只看平均值
flowchart TB
    start["週期處理會遲到"] --> c1["確認等待方式"]
    c1 --> c2["確認 fast path 是否塞了重活"]
    c2 --> c3["確認佇列上限與溢位方針"]
    c3 --> c4["確認優先順序與 MMCSS 的用法"]
    c4 --> c5["確認電源模式與 timer / power throttling"]
    c5 --> c6["還是在抖,量測 DPC / ISR / page fault"]

接下來依序看各項。

4.2. 週期迴圈不要靠 Sleep

首先要確認的就是這裡。 Sleep(1) 不是「剛好等 1ms」,而是 至少等 1ms 以上。 再加上 Step() 的執行時間,週期的誤差會直接累積。

flowchart LR
    a["Sleep(1)"] --> b["可能等比預期更久"]
    b --> c["加上 Step 的執行時間"]
    c --> d["週期誤差累積"]

週期處理要以 絕對期限 而不是相對時間來跑,才會穩定。 概念像這樣:

int64_t next = QpcNow() + periodTicks;

while (running)
{
    WaitUntil(next - wakeMarginTicks);   // event / waitable timer
    while (QpcNow() < next)
    {
        CpuRelax();                      // 最後才短暫 spin
    }

    int64_t started = QpcNow();
    FastStep();                          // no blocking, no alloc, no heavy lock
    int64_t finished = QpcNow();

    RecordTiming(next, started, finished);

    next += periodTicks;

    while (finished > next)
    {
        ++missedDeadlines;
        next += periodTicks;
    }
}

重點有 2 個:

  • 不要每次都 next = now + period
  • 遲到時的處置事先決定好

想在普通 Windows 上穩定下來,這裡效益相當大。

4.3. 切開 fast path 與 slow path

就結構來說,效益最大的是 把對時間嚴苛的處理與可以稍晚的處理切開

  • fast path 每週期必經、希望很快結束的處理
  • slow path 儲存、通訊、整理、彙總、UI 等可以稍晚的處理
flowchart LR
    dev["裝置 / 計時器 / 事件"] --> fast["fast path:取得 / 控制 / 最小複製"]
    fast --> queue["固定長度佇列"]
    queue --> slow["slow path:整理 / 儲存 / 送信 / UI"]
    fast --> stat["時間紀錄 / miss 紀錄"]
    stat --> slow

fast path 只做下列動作:

  • 取資料
  • 計算控制值
  • 最小限度的複製
  • 時間戳記
  • 丟進佇列
  • 記錄 miss 或 overrun

其他交給 slow path 比較穩。 在普通 Windows 上追求軟即時,這個切分就是地基

4.4. 佇列用固定長度,先決定溢位方針

一想到「遲到就塞佇列」,就很容易把問題往後推。 無上限佇列看似安全,實際上往往只是 把延遲藏起來

flowchart LR
    input["fast path 產生資料"] --> q{"固定長度佇列是否有空"}
    q --> enq["有空:直接放入"]
    q --> policy["沒空:套用方針"]
    policy --> latest["最新值優先:丟掉舊資料"]
    policy --> strict["不允許遺漏:錯誤 / 停止 / 警報"]
    policy --> logonly["日誌用途:記錄掉落數"]

事先要決定的是這 3 件事:

  • 佇列上限
  • 溢位時的方針
  • 如何記錄溢位事實

例如只有最新值有意義,那就丟掉舊的比較自然。 若完全不能遺漏,與其默默延後,不如當成錯誤處理,後面會省事。

4.5. 不要把重活放進 fast path

fast path 要避開的,其實相當明確:

  • 檔案寫入
  • 網路送信
  • DB 寫入
  • 重的日誌輸出
  • 每次 new / malloc / List<T>.Add
  • 每次字串串接或 ToString()
  • 重鎖
  • 容易在首次存取引起 page fault 的處理

總結:不要把可能變慢的處理帶進 fast path

特別要注意的是下列 3 項:

  1. 配置與釋放 fast path 應該先配置緩衝區再重複使用,比較穩。

  2. 阻塞 I/O 在開發機上看起來夠快,正式環境常常會抖。

  3. Page fault 啟動時先摸一次必要的記憶體,就能有差。

必要時也會用到 VirtualLock,但那只是輔助。 首先要把 fast path 本身變輕、並預先準備好必要記憶體,順序在先。

4.6. 優先順序只提高必要執行緒

優先順序的基本原則是 不要全部提高。 先只針對真正對時間敏感的執行緒。

思路大致是:

  • UI 與一般 worker 維持普通優先順序
  • 只對 fast path 執行緒按需提高
  • 儲存、送信、日誌彙整這類後端工作考慮 background mode
  • 不先動整個行程,先從執行緒單位思考

音訊、影像這類持續串流,先考慮 MMCSS 會比較自然。 MMCSS 是 Windows 為多媒體準備的排程機制。 比起單純硬調高優先順序,這比較符合 Windows 的做法。

另一方面,REALTIME_PRIORITY_CLASS 不是一開始就上的設定。 有時會有效,但副作用也大;在專用機上充分驗證行為,只在真正必要時才考慮 比較安全。

4.7. 計時器、CPU、電源設定要一起看

在普通 Windows 上,光改程式碼往往還不夠。 等待方式、CPU 的配置、電源設定最好一起看,比較清楚。

要確認的點:

  • 經過時間量測有沒有用 QueryPerformanceCounter / Stopwatch
  • 等待能否使用裝置事件或高精度 waitable timer
  • 若用 timeBeginPeriod,是否只在必要期間使用
  • 是否根據量測結果決定要不要釘 CPU
  • AC 供電、電源模式、process power throttling 是否確認過

關於 CPU,一開始就用 hard affinity 釘住,不如先從 SetThreadIdealProcessor 或 CPU Sets 這類 較寬鬆的指定 開始,通常比較好處理。 釘死反而會減少 OS 的喘息空間。

4.8. 把遲到呈現出來

最後重要的是 不要隱藏遲到的事實。 週期違規與其當例外丟掉,不如計數並記錄,後面比較好追根究柢。

最起碼保有這些:

  • 預定開始時刻
  • 實際開始時刻
  • 實際結束時刻
  • lateness
  • 執行時間
  • missed deadline 次數
  • 連續 missed deadline 次數
  • queue 深度
  • 掉落數

即使這樣仍有大尖峰,就要懷疑應用程式以外的因素。 普通 Windows 可能因 DPC / ISR、驅動、page fault、溫度、時脈變動 而抖。 到這階段,用 ETW / WPR / WPA 或 LatencyMon 分析,比較容易看到原因。

5. 電源設定、OS 設定檢查清單

在普通 Windows 上容易生效的設定,主要落在 Windows 標準範圍內。 比起一開始就進 BIOS / UEFI,依下列順序看會比較可重現。

flowchart LR
    a["確認 AC 供電與電源模式"] --> b["必要時建立專用電源計畫"]
    b --> c["確認 power throttling 與 timer 相關"]
    c --> d["降低背景負載"]
    d --> e["有需要時再看 BIOS / UEFI"]

做成檢查清單如下:

  1. 用 AC 供電 用電池驅動調教,結果不易穩定。

  2. 確認 [設定] > [系統] > [電源與電池] 的電源模式 正式或量測時,用偏 [最佳效能] 會比較直觀。

  3. 必要時準備專用電源計畫 日常用平衡,只有正式/量測時切專用計畫,實務上好管理。

  4. 確認最低處理器狀態 / 最高處理器狀態 在專用機或量測時,AC 情況下試試 100% / 100% 值得一試。 不過熱、耗電、風扇噪音會上升。

  5. 確認 process power throttling / EcoQoS 若套用了省電方向的設定,可能影響短時間等待或執行速度。

  6. 減少不必要的背景負載 雲端同步、自動更新、重型瀏覽器、常駐監控、索引建立等照樣會影響。

  7. 最小化或接近不可見狀態下也測一下 Windows 11 上,不可見的應用程式 timer resolution 行為可能改變。

  8. BIOS / UEFI 放最後 C-state 或廠商獨有的靜音/Eco 設定可能有效,但機型依賴高。 先在 Windows 側調完再碰,比較好掌握。

6. 量測與評估

6.1. 要記錄什麼

至少想記錄這些:

  • 週期預定時刻
  • 實際開始時刻
  • 實際結束時刻
  • lateness
  • 執行時間
  • missed deadline 次數
  • 連續 missed deadline 次數
  • queue 深度
  • 掉落數
  • DPC / ISR 尖峰
  • page fault
  • 溫度 / 時脈變動

只看平均,在正式環境裡會困擾的那種延遲很容易被蓋掉。 普通 Windows 上出問題的,常常是 平均很快,但偶爾大幅度延遲 這種形態。

6.2. p99 / p99.9 / max 是什麼

這類用語先理解意義,後面會順很多。

  • 平均 看整體大致傾向的指標。 但偶爾冒出的大延遲容易被埋沒。

  • p99 99% 的樣本落在此值以下 的分界。 量了 1000 次,大概可理解為 去掉最慢的 10 次後的上限附近

  • p99.9 99.9% 的樣本落在此值以下 的分界。 量了 1000 次,大概是 去掉最慢的 1 次後的上限附近

  • max 表現最差那一次的值。

flowchart LR
    sample["收集 1000 次的延遲"] --> sort["由小到大排序"]
    sort --> p99["p99:第 990 位附近"]
    sort --> p999["p99.9:第 999 位附近"]
    sort --> max["max:第 1000 位"]

實務上除了平均,也看 p99 / p99.9 / max,比較貼近體感。 想把「平常還好,偶爾會卡」量化,這種看法很好用。

注意一點:要看 p99.9,樣本數也要夠。 10Hz 的處理,要湊 1000 個樣本就要 100 秒。 樣本太少就看 p99.9,會被偶發的 1 次強烈拉走。

6.3. 用什麼看

工具大致是:

  • 應用程式內量測 先自己取 period、lateness、execution time、queue depth

  • ETW / WPR / WPA 看 CPU、context switch、DPC / ISR、page fault

  • LatencyMon 對驅動源頭的抖動做初步定位

  • 溫度 / 時脈監控 觀察熱或降頻的影響

先靠應用程式內量測,區分是自己處理重,還是被外部擋住。 確認之後再用 ETW / WPR / WPA 追 DPC / ISR 或 page fault 的影響會更有效率。

6.4. 測試條件

測試只用安靜的開發機不夠。 至少下列條件要分開看:

  • 啟動後暖機前
  • 暖機後
  • 長時間連續運行
  • UI 在前景
  • UI 最小化 / 近乎不可見
  • AC 供電
  • 電池驅動
  • 有網路或磁碟負載的狀態

不在接近正式的條件下觀察,事後很容易發生「開發機上明明沒事」。

7. 粗略的分類

在普通 Windows 上,現實的目標值大致可如下整理。

  • 10~20ms 級,偶爾抖動能吸收 切 fast / slow、固定長度佇列、一般~略高優先順序、事件驅動就足夠。

  • 1~5ms 級,要持續跟上 把 fast path 做到免配置、專用執行緒、MMCSS 或謹慎的優先順序調整、高精度 waitable timer、AC 供電、偏最佳效能的電源設定,要一起看。

  • 接近 1ms 以下,而且長時間高負載都不能漏 光靠普通 Windows 的 user-mode 應用程式單獨做就吃緊。 要把關鍵部分設計到別處逃生比較好。

  • GUI、日誌、通訊、DB 全部擠在一起 不要一個行程一個迴圈全包,拆開職責比較穩。 後段的情況容易把前段期限弄壞。

8. 總結

在普通 Windows 上追求軟即時時,首先要看的是:

  1. 週期迴圈是不是在依賴 Sleep
  2. fast path 與 slow path 有沒有切開
  3. 佇列是不是固定長度,溢位方針有沒有定
  4. fast path 有沒有混進 I/O、配置、重鎖
  5. 優先順序或 MMCSS 是不是只用在需要的執行緒
  6. 普通 Windows 的電源設定與量測有沒有整合到位

在普通 Windows 上,光靠優先順序就穩定的情況其實不多。 把設計、等待方式、電源設定、量測湊齊,才真正成為不易延遲的形狀。

反過來說,按這個順序整理,即使是普通 Windows,也能把 soft real-time 做到相當實用的地步。

9. 參考資料

相關文章

共用相同標籤的最新文章。能以相近的主題延伸理解。

相關主題

與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。

與本主題相關的服務

本文連結到以下服務頁面,歡迎從最接近的入口查看。

回到部落格一覽