Multimedia Class Scheduler Service (MMCSS) (1)

今回は,Windows Vista で導入される Multimedia Class Scheduler Service (以下 MMCSS) について取り上げてみたいと思います.その前に,Windows のスレッドスケジューリングについて復習といきましょう.
現在の Windows は,実行スレッドを頻繁に入れ替えることで,いわゆるプリエンプティブなマルチタスク環境を実現している,という話は世間でも広く知られているかと思います.
しかし,実際にどのようなタイミングで実行スレッドが切り替えられているかは,意外と知られていないのではないでしょうか.ヒントは例によって『インサイド Microsoft Windows 第4版〈上〉』に見つかります.

Windowsのスケジューリングコードは、カーネルに実装されています。しかし、単一の「スケジューラ」モジュールやルーチンというものはありません。実装コードは、カーネル内に散在するように置かれ、その中でスケジューリング関連イベントを発生させる方式になっています。これらの機能を実行するルーチンは、集合的に、カーネルディスパッチャと呼ばれています。次のイベントはスレッドディスパッチャを必要とします。

  • スレッドがレディ状態に入ったとき。たとえば、あるスレッドが新規に作成されたり、待機状態から抜け出した直後など。
  • タイムスライスの経過、動作の完了、実行権の放棄、あるいは待機状態に入ったスレッドが実行状態を抜け出したとき。
  • スレッドの優先度が変化したとき。この変化は、システムサービスが呼び出されたときなどにも発生する。また、Windows自身が優先度を変更することもある。
  • スレッドのプロセッサアフィニティが変化し、実行中のプロセッサ上でそれ以上動作できなくなったとき。

アプリケーションから見れば,大まかに 2 種類のスレッド切り替えが存在します.

  1. アプリケーションが API を呼び出し,カーネルモードに移行した結果,カーネルディスパッチャが実行される
  2. タイマ割り込みの結果,カーネルディスパッチャが実行される

前者は,例えばスレッド操作 API であったり,Sleep(0) や SwitchToThread のような実行権を手放す API であったり,WaitMessage や WaitForSingleObject のような明示的な待機 API であったり,ReadFile API のような読み取りが完了するまで内部的に待機する API などが該当するでしょう.これらの状況に共通するのは,あるイベントが発火するのを待っていたり,HDD からのデータ読み取りが完了するのを待っていたりと,そのスレッドの実行をしばらく中断しても問題がない場合です.このような場合,カーネルディスパッチャは,処理の実行を希望している別のスレッドに実行権を移動させます.
ソースコード中にこれらの API が見あたらなくても,例えば C 言語の fread 関数は内部で ReadFile API を呼び出しているでしょうから,単純な C 言語のプログラムでもカーネルディスパッチャの動作について注意を払っておくのことには意味があります.
一方,今回の話で興味があるのは後者の方です.Windows カーネルは,一定時間ごとに割り込みを発生させる周期タイマをカーネルディスパッチャのトリガに使用することで,前述のような API 呼出しが一切行われなかった場合でも強制的にスレッド実行権を移動させられるようにしています.いわゆるプリエンプティブなマルチタスク環境の解説で意図されているのはこちらの仕組みの方でしょう.また,タイマ割り込みは経過時間のカウントにも使用されているので,時間指定された待機動作にも大きく関係しています.
周期タイマは HAL が隠蔽していますが,多くの x86 Windows 環境では 8254 CMOS チップ互換の PIT が使用されています.Local APIC タイマや High Precision Event Timer (HPET) も同用途に使用可能ですが,最近の Windows 環境でどこまで使われるようになってきているのかはよく分かりません(文章末の参考資料[2],[3]を参照してください).
『インサイド Microsoft Windows 第4版〈上〉』の「図3-7 DPC 処理の概要」にあるように,PIT がハードウェア割り込みを発生させると,経過時間の計測が行われ,時間待ちをしていたスレッドの解放が行われます.ただしスレッドディスパッチ処理はすぐには行われず,カーネルは DPC (遅延プロシージャコール) を待ち行列に入れます.その後,IRQL が DPC/ディスパッチレベル以下になったタイミングで,未処理の DPC 行列が全て処理されます.このタイミングで,カーネルディスパッチャは必要に応じて実行スレッドの切り替えを行います.つまり,周期タイマをトリガとしたプリエンプティブなスレッド切り替えの時間粒度は,周期タイマの割り込み間隔に依存することになります.例えば 15 msec の周期でタイマ割り込みが発生する環境で,Sleep(3) (最低約 3 msec スリープせよ) を実行した場合,そのスレッドが活性化すべきかどうかのチェックが最大 15 msec 後になる可能性があるわけです.
いわゆる「Sleep の精度問題」google:Sleep 精度 は,この現象について逆さまから見たものと考えられます.
「Sleep の精度問題」では,「timeBeginPeriod / timeEndPeriod API を使用することで Sleep の精度が向上する」とまとめられていることが多いですが,因果関係としては次のようになっているのでしょう.

  1. timeBeginPeriod(1) のような小さい値を設定することで,PIT の割り込み間隔が短くなる
  2. 割り込みによってトリガされる経過時間の計測間隔・カーネルディスパッチャの実行間隔が短くなる
  3. Sleep によって待機していたスレッドの活性化タイミングが,意図していたタイミングに近くなる

注意して欲しいのは,timeBeginPeriod は Sleep の精度を向上させるための API ではないということです.あるプロセスが timeBeginPeriod を呼び出した結果,システム全体のスレッドスケジューリングのパターンが一変してしまい,あくまでその結果として Sleep の精度が向上したと見るべきでしょう.
身近な例として,Windows Media Player で音声ファイルやムービーを再生すると,他のアプリケーションが滑らかに動くようになったという話などは,このことが影響しているものと考えられます.
一般に,PIT の割り込みタイミングを短くした方が個々の処理のレイテンシは改善されますが,コンテキストスイッチが多発することで全体のスループットは悪化する恐れがあります(文章末の参考資料[1],[2]を参照してください).その点を考慮して,一般的な Windows 環境では 10 msec または 15 msec 程度の値がデフォルト値となっています(参考資料[2]).
Windows ゲームについても,レスポンスを向上させたり,時間揺らぎを減らしたりする目的で,しばしば timeBeginPeriod(1) が使用されます.Direct3D 9 では,D3DPRESENT_PARAMETERS::PresentationInterval に D3DPRESENT_INTERVAL_ONE を指定することで,割り込み間隔を短くするために内部的に timeBeginPeriod が呼び出されます.

Full-screen mode supports similar usage as windowed mode by supporting D3DPRESENT_INTERVAL_IMMEDIATE regardless of the refresh rate or swap effect. D3DPRESENT_INTERVAL_DEFAULT uses the default system timer resolution whereas the D3DPRESENT_INTERVAL_ONE calls timeBeginPeriod to enhance system timer resolution. This improves the quality of vertical sync, but consumes slightly more processing time. Both parameters attempt to synchronize vertically.



参考資料.

  1. id:NyaRuRu:20051226
  2. http://www.microsoft.com/whdc/system/CEC/mm-timer.mspx
  3. http://mowamowa.p.utmc.or.jp/~amedama/cgi-bin/wiki/wiki.cgi?page=Kernel%A5%E1%A5%E2+%BB%FE%B4%D6%B4%C9%CD%FD%CA%D4

Multimedia Class Scheduler Service (MMCSS) (2)

Multimedia Class Scheduler Service は,プライオリティの低いタスクを邪魔することなくリアルタイム指向のタスクを実行するための仕組みで,google:Windows Audio Video Excellence (WAVE) に関連して Windows Vista で導入されました.
従来の Win32 スレッドがスレッド優先度を直接設定していたのに対し,MMCSS は事前に典型的なタスクの種類を定義し,大まかにはそのタスクを選択することで適切な優先度を設定するという方式をとります.また,低優先度タスクのために最低 10% (デフォルトでは 20%) の CPU 時間が確保されています.
タスクには GPU の優先度など,従来にはなかった優先度も設定可能になっています*1
設定可能なタスクはレジストリの HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Multimedia\SystemProfile\Tasks 以下に定義され,事前定義されているタスクは以下の通りです.

Task Name Scheduling Category SFIO Priority Background Only Priority Background Priority Clock Rate GPU Priority Affinity
Audio Medium Normal True 6 10000 8 0
Capture Medium Normal True 5 10000 8 0
Distribution Medium Normal True 4 10000 8 0
Games Medium Normal False 2 10000 8 0
Playback Medium Normal False 3 4 10000 8 0
Pro Audio High Normal False 1 10000 8 0
Window Manager Medium Normal True 5 10000 8 0

また,HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Multimedia\SystemProfile\SystemResponsiveness には低優先度スレッドに予約する CPU リソースを % 単位で指定します.この値は 10% 単位で丸められ,10% 以下は 10% と扱われます.

*1:IDirect3DDevice9Ex::SetGPUThreadPriority との関連は不明

Multimedia Class Scheduler Service (MMCSS) (3)

事前定義されたタスクの Clock Rate はいずれも 10000 となっていますが,これは 100 ns 単位なので 1 msec に対応します.このことから,MMCSS を有効化するとスレッドスケジューリングに使用される割り込み周期は少なくとも 1 msec 以下に再設定されることが予想されます.実際に実験してみました.テストには Windows Vista RC 1 日本語版,Thinkpad T60 を使用しました.
まず,この Vista RC 1 環境でのデフォルト状態でのタイマ割り込み間隔について見てみます.
Sleep(1) の呼出しから復帰する時間を観察すると,この環境のタイマ割り込みは約 15 msec のようでした.以前の Windows でも,x86 ユニプロセッサ環境では 10 msec,x86 マルチプロセッサ環境では 15 msec がデフォルトと言われていましたから*1,この値は妥当なものだと考えられます.
次に,timeBeginPeriod(1) を呼出したところ,Sleep(1) の呼出しは 1 msec 程度の時間で帰るようになりました.
実際に割り込み回数が変化していることは,Process Explorer からも分かります.

デフォルト状態
timeBeginPeriod(1)実行後

上が平常時で,下があるプロセスで timeBeginPeriod(1) を呼び出した後のものです.Interrupts の 1 秒間の Context Switch Delta が増えていることがおわかりになるかと思います.
ではいよいよ,MMCSS を使用してみましょう.timeBeginPeriod(1) を呼び出す代わりに,AvSetMmThreadCharacteristics API を使用してスレッドに Games タスクを割り当てたところ,やはり timeBeginPeriod(1) と同様の効果があることが分かりました.このことから,Windows Vista では timeBeginPeriod(1) よりも AvSetMmThreadCharacteristics を使用する方が行儀の良いプログラムと呼べるかもしれません.
もうひとつおまけです.事前定義されたタスクに "Window Manager" があることからも分かるように,MMCSS は Desktop Window Manager (DWM) も想定しています.しかし,Aero が有効な状態で行った最初の計測では割り込み間隔 15 msec のように振る舞っていたことから,デフォルトでは DWM は MMCSS を使用していないと考えられます.
DWM に MMCSS を使用させるには,DwmEnableMMCSS API に TRUE を渡します.この場合も,Sleep(1) の呼出しは 1 msec 程度の時間で帰るようになったことが確認できました.

まとめ

  • 現在の Windows の実装では,timeBeginPeriod(1) を呼び出すことで*2,スレッドスケジューリングの粒度が変化する.
  • Windows Vista では AvSetMmThreadCharacteristics を指定することで,timeBeginPeriod API の呼出しをより汎用的に置き換えることが出来る.
  • DWM に MMCSS を使用させるには,DwmEnableMMCSS API を使用する.

*1:『インサイド Microsoft Windows 第4版〈上〉』の「6.5.8 タイムスライス」

*2:より汎用的に書くなら,timeGetDevCaps で得られた最小分解能を指定するべきでしょう