monitor + mwaitは使いものになるのか?

以前から、mwaitは簡単に使えるようになったら嬉しいのではないか?と、思っていたのだが、社内で、「実際mwaitってどうよ?」とかメールが流れてたので、よい機会だからなんか試してみた。

http://int.main.jp/files/devmwait.tar.gz

/dev/mwaitをつくるドライバと、それを実験するユーザプログラムが入っていて、なんか時間とかを計測できます。

色々計測した感想としては、まあ、無理して使うほどのものでもないかなぁ…というところだった。

monitor, mwaitとは何か

x86のマルチスレッドプログラムで、効率よくメモリポーリングを実現する仕組み。


複数のスレッドが通信しあってるとして、別のスレッドがフラグを立てるのを待つ、とかするのは、たまによくあるパターンである。

普通にメモリポーリングを書くと、

volatile int *ptr;
while (1) {
    if (*ptr == check_value)
        break;
}

というような処理になる。


これはふたつの点で問題がある

  • CPU時間使うので、他のスレッドにCPU時間が渡せなくなる
  • 電力の無駄

で、今のプロセッサは、コヒーレンシ維持のために、他のコアによるメモリの書きかえを専用ハードウェアで監視することが可能なので、「メモリ上のフラグが立つまで待つ」という処理は、ビジーループを回すよりも、もっと効率良く実装できるはずである。

これをなんとかするのがmonitor/mwaitで、プロセッサをスリープ状態に入れながら、メモリの書きかえを監視できるようになる。これを使えば、上の問題のうち、「電力の無駄」のほうは、改善できる。

使いかたは、

  1. monitor で監視したいアドレスを指定
  2. mwait で監視アドレスが書きかわるまで待機

となっていて、

addr = 監視したいキャッシュラインのアドレス;
while (1) {
    monitor(addr);
    if (*addr == check_value)
        break;
    mwait(c_state);
    if (*addr == check_value)
        break;
}

と、いうような感じにする。

c_state は、x86のスリープの状態の指定で…えーと…よくわかってないので各自調べてください(マニュアルの3Aに書いてある気がする)。

また、mwaitは、割り込みが来てもスリープから帰ってくるので、デバイスを待つことも可能。

あとSMI割り込みが入ってもスリープから帰ってくるので、なんかよくわからんけどスリープが解除されることもある。

/dev/mwait

と、いうのがmwaitなのだが、この monitor + mwait はRing0(OS)からしか使うことができなくて、「う〜ん、あるのに使えないってもったいないなぁ」とか、ずっと思っていて、/dev/mwait作ってioctlするだけとかなんか、そんなのをよく妄想していたのだが、社内で「誰かmwait実測したことある人おらんか?」とかメールが来たので、よい機会だから作ってみた。
のが、上のファイルである。


使いかたは、

  1. 上のファイルを展開したディレクトリで $ sh build.sh する。
  2. mwait.ko ができるので、それをinsmodする。
  3. /dev/mwait ができるので、それをopenして、ioctlする


ioctlの使いかたはDO_MWAITすればよいくて、パラメータは、

struct mwait_request {
    int *addr; // 監視したいラインのアドレス
    __u32 mask; // 監視したい値
    __u32 mwait_eax; // monitor するときの eax の値(C stateの値。詳細はマニュアル2Aのmonitorのところ)
    __u32 mwait_ecx; // monitor するときの ecx の値(0でよい。詳細はマニュアルのmonitorのところ)
    __u32 interrupt_threashold; // 何回起きたときにsleepに入るか。小さいほど他のタスクにCPU時間を譲りやすい
};

とかやる。

計測プログラムが、user/test-mwait.cにあるので、それを見てもらえれば。


で、interrupt_threasholdだが、上で述べたように、ビジーループには、

  • CPU時間使うので、他のスレッドにCPU時間が渡せなくなる
  • 電力の無駄

というふたつの問題があるのだが、電力は、まあいいとして、mwaitを使っても、CPU時間が別スレッドに譲られるわけではないので、上のほうの問題が解決しないので、なんとかして他のスレッドに処理を譲れないかと思って色々試した値。


中の、待ちをする部分が、

trycount = 0;
while (1) {
    monitor(req.addr);
    if (*req.addr == check_value)
        break;
    mwait(req.c_state);
    if (*req.addr == check_value)
        break;
    if (trycount > req.interrupt_threashold) {
        msleep(1);
        trycount = 0;
    } else {
        cond_resched();
    }
}

みたいな感じになっていて、ある程度失敗すると、msleepするようになっていて、この値を調整することで、なんとかならんかなぁ、というのを試していた。

結論としては、msleepするぐらいなら、futexで待ったほうが速いし安定する、という感じで、あんまりうまくいかなかった。

(Documentation/timers/timers-howto.txtによると、まあ、なんか色々あるらしいので、ひととおり試したが、どれもあまりうまくいかなかった)

experimental results

で、社内のメールには、「Atom で動かして hoge [usec] ぐらいでなんとかならん?」とか書いてあったので、hoge [usec]にならんか、というのを試した。結論としては無理だった。


CPU は i5-2400S

user/test-mwait.c を動かして、

  • user space busy loop
  • futex
  • mwait (interrupt_threashold == 1)
  • mwait (interrupt_threashold == 100)

で、

  • 動かしてワットチェッカ読みで消費電力(アイドル時:89W)
  • 動かして往復のレイテンシ
  • バックグラウンドでmake -j4 で linuxビルドしてその時間

を計測した。

大体、

(※ i = interrupt_threashold)

 ** 通信レイテンシ 
   - busy loop       : 0.15[usec]     (200clk)
   - futex           : 45-100[usec]   (115000clk - 254000clk)
   - mwait(i == 1)   : 2.8-3700[usec] (4800clk - 9240000clk)
   - mwait(i == 100) : 2.7[usec]      (4500clk)

 ** 消費電力
(…あ、しまった…これディスプレイ含んでる…ディスプレイ50Wぐらいです…)
   - busy loop       : 97W
   - futex           : 89W
   - mwait(i == 1)   : 89W
   - mwait(i == 100) : 89W

 ** カーネルビルド
   - busy loop       :  143.4 secs (3.004 CPUs utilized)
   - futex           :  111.3 secs (3.885 CPUs utilized)
   - mwait(i == 1)   :  126.4 secs (3.420 CPUs utilized)
   - mwait(i == 100) :  146.8 secs (2.935 CPUs utilized)

みたいな感じで、

  • mwaitを使うと、busy loopより大分遅い
  • interrupt_threasholdが少ないと、最悪値3700usecとか出てて安定しなさすぎ
  • interrupt_threasholdが多いと、全然他のスレッドに時間譲れてない、むしろbusy loopよりちょっと負担大きい(カーネルビルド時間は3回くらい測って似た傾向だったので…)

と、いうような感じだった。futexの100[usec]のオーバーヘッドが性能にクリティカルな場面だと、mwaitの2[usec]ってかなり許せない値の気がするし、電力とか、ビジーループしても、そんな無理するほど変わるというわけでもないし、まあ、無理して使うほどでもないかな…という感じだった。



あと、Atom-N550でもはかっていて、

 ** 通信レイテンシ 
   - busy loop       : 3.5[usec]     (2200clk)
   - futex           : 60-80[usec]   (90000clk - 120000clk)
   - mwait           : 7.1[usec]     (7600clk)

とか、メモに書いてある。


ただ、Atomだとシステムコール呼ぶだけで3[usec]ぐらいかかるみたいなので、

clock_gettime + ioctl = 6[usec]

ぐらいのオーバーヘッドがある。

Linuxだとioctlは消せないが、clock_gettimeは消せるので、
実質 4[usec] ぐらいではないかと思う。

まとめ

まあ、無理して使うほどでもないか、という感じだった。

PCがバッテリで動いていて、マルチスレッドで、100[usec]のオーバーヘッドが許せなくて、3[usec]のオーバーヘッドが許せる場合には、mwaitを使うのも選択肢のひとつとして考えてもよいかもしれない。