Archives

October 2008 の記事

Windows の Interlocked 系 API は atomic な操作に使われます。
例えば InterlockedIncrement() は load と store を含みますが、その間に他の
スレッドが同じメモリを書き換えることなく処理が完了するよう調整されます。

x64 でコンパイルするとこれらの Interlocked API はインライン展開されるようです。
下記のように同機能の intrinsic 命令が用意されおり、x64 では単なる別名として
定義されていました。

InterlockedCompareExchange Function
_InterlockedCompareExchange Intrinsic Functions

x86 でも直接 _InterlockedCompareExchange() を使えば組み込み命令として機能します。
実際のコードは下記の通り。

x64:
 lock cmpxchg dword ptr [mem32],edx  // InterlockedCompareExchange
 lock cmpxchg qword ptr [mem64],rdx  // InterlockedCompareExchange64

x86:
 lock cmpxchg dword ptr [mem32],edx   // InterlockedCompareExchange
 lock cmpxchg8b qword ptr [mem64]     // InterlockedCompareExchange64

InterlockedIncrement() + x64 の場合

// 返値参照あり
mov ecx,1
lock xadd    dword ptr [mem],ecx
inc ecx

// 返値参照無し
lock add     dword ptr [mem],1

返値を参照するかどうかによって、命令そのものも置き換わっています。
単なる関数や asm 文の inline ではなく、組み込み命令であることがよくわかります。

以前紹介した Gamefest2008 のスライドによると DirectX11 の Compute Shader
には下記の命令が追加されるようです。

 InterlockedAdd()
 InterlockedMin()
 InterlockedMax()
 InterlockedOr()
 InterlockedXor()
 InterlockedCompareWrite()
 InterlockedCompareExchange()

Gamefest 2008 Presentations
「Direct3D 11 Computer Shader More Generality for Advanced Techniques」

InterlockedMin()/InterlockedMax() は Win32 API に無い命令です。
書いてみるとこんな感じでしょうか。(厳密な動作は未検証)

template<typename T>
void InterlockedMin( T volatile* mem, T val )
{
    union {
        T      fval;
        long   ival;
    }	cur;
    do{
        cur.fval= *mem;
        if( cur.fval <= val ){
            break;
        }
    }while( _InterlockedCompareExchange(
                reinterpret_cast<long volatile*>( mem ),
                *reinterpret_cast<unsigned long*>( &val ),
                cur.ival ) != cur.ival );
}


関連エントリ
Direct3D11 Compute Shader など
SSE3 の monitor mwait 命令



SSE3 で monitor / mwait 命令が追加されています。
HT やマルチコアなど、ハードウエアスレッドの同期効率を上げるために有効
らしいですが、使ったことがないので調べてみました。

monitor で監視アドレスを与えた後、mwait 命令は監視アドレスに何らかの
書き込みがあるまで待機します。
メモリの更新をハード的に検出する動作は RISC 系の atomic 命令 ll/sc に
似ています。

イベント待ちのような動作なので様々な通知に使えそうです。ただこれが OS では
なく CPU そのものの命令で実行されるため、いくつかの疑問も生じます。
CPU レベルの命令でアプリケーションがこのような動作を行って問題無いのか、
割り込みやコンテキストスイッチ等でどのように振る舞うのか、
mwait に復帰できるのか、monitor の状態がリストアされるのか、など。

intrin.h を include するだけで、_mm_monitor() / _mm_mwait() の
intrinsic 命令が使えます。
実際に x86 でも x64 でもコンパイルは可能ですが、無効な命令となり実行は
出来ませんでした。
CPUID を調べると、MONITOR/MWAIT に対応していることは確認できます。

Intel 64 and IA-32 Architectures Software Developer's Manuals
日本語技術資料のダウンロード

マニュアルによるとどうやら特権命令らしく、無条件で使えるのはレベル 0
の場合のみ、特権レベル 1~3 での動作も可能だけど、そのためには何らかの
条件がいるそうです。
また MONITOR/WAIT が使えるかどうかは MSR IA32_MISC_ENABLES (1a0) の
bit18 の設定にも依存し、この値が 0 だと CPUID のフラグも落ちるとのこと。
この値も設定されており問題はなさそうです。

Manual の Volume 3A 7.11.3 で、特権レベル 1 以上の場合でこれらの命令が
使えるかどうか判定する方法が載っています。intrinsic で書くと次の通り。

__try{
    _mm_monitor( memory, 0, 0 );
}
__except( 1 ){
   // 使用できない
}

つまり Illegal Instruction が発生するのは特権レベル 0 以外は使えないことを
意味しているようです。何らかの設定で無効にされているのか、それとも
もともと使えないものなのか、その条件はわかりませんでした。

マニュアルを見ると mwait の動作は、割り込み等によって頻繁に解除される
ようです。mwait に復帰することはなく、監視したメモリの値を調べて解除原因を
判断する必要があるようです。

また監視メモリの領域は CPU 依存で、CPUID の 0x05 で調べることが出来ます。
手持ちの CPU では下記の通り。最小も最大も 0x40 == 64 でした。

CPUID (CPU-Z によるレジスタダンプ)
・Atom N270
 0x00000005 0x00000040 0x00000040 0x00000003 0x00020220
・Core2 Duo E6600
 0x00000005 0x00000040 0x00000040 0x00000003 0x00000020

よって何らかの変数を監視するよう割り当てても、同じ 64byte 以内に
割り当てられた他の変数へアクセスによって解除される可能性もあります。

(1) 判定用メモリ領域 64byte (CPUIDで判定) を用意して初期値を入れる
(2) monitor 実行
(3) monitor 命令実行中に書き換えられている可能性があるので判定する。
  書き換わっていたら値によって適切な分岐。
(4) 書き換わっていなければ mwait
(5) 解除されたらその要因を調べる。
  値が書き換わっていたら値によって分岐。
  書き換わっていなければ (2) へ戻る。

もし使うならこのような手順になるようです。
つまりスピンロック時に監視アドレスを与えて停止させられるため、
ループ中に導入することでバスアクセスを減らし、効率を上げることが出来ます。
でも使えませんでした。