少なくとも命令に消費したクロック数を知ることができる関数であり
一見最高精度の時計関数が作れそうだが
ノートパソコンの可変周波数やAthlon64等のDualコアなどでは値がずれるため
RDTSCとほぼそれをベースに実装されているQueryPerformanceCounterを使ったアプリは
うまく動かないという話をじばしばみかける。
但しPentium3や4などちょっと前のCPUにおいてはほぼ間違いなく動く関数なので
あくまで開発者が実験的時間測定に使う関数という位置付けでは無いかと思う。
とりあえず今回はシングルCPUで間違いなく動くPentium4 1.8GHz、Windows2000というカタイ環境でのみのテスト
グダグダ憶測で最適化するよりこれを使ったほうが良いと思う。
RDTSCは「Read Time Stamp Counter」を縮めたものだそうでオペコードは$0F31だが
delphi6以降ならば普通にインラインで書けば実行してくれるが
命令は先読みされるので不正確になる可能性がある。
そこでオーダリングを固定するためにシリアライズ命令を何か入れるというのが基本で
大抵の場合はcpuidを使う。
RDTSC自体cpuidでサポートされているか調べないといけないような話もあるが
私の話はSSE対応を最低レベルとしているので不必要だ。
従来負荷試験などは繰り返し数を100万回とかやって精度を上げるのだが
クロック数が得られる場合、命令のオーダーを調整すれば
OSにスレッドをぶった切られない限り同じ値が出るはずだ。
低スペック環境(2008年ベース)での高速ルーチンのチューニング等にRDTSCは使えると思った。
EAXとEDXに64bitで結果が返るのでfastcallの場合、グローバル関数を使わないならば以下のようになる。
基本
Procedure getrdtsc(p_cnt:Pointer);
asm
push eax
cpuid
pop ecx
rdtsc
Mov [ECX],EAX
Mov [ECX+4],EDX
end;
これは単独関数化したケース。
cpuidがレジスタ破壊するのでEAXはECXへの退避は出来ない。
スタックを使ったがグローバル変数か、クラス変数を利用するのが良いのであろう。
クロック差分を取ることに徹した関数を考えてみた。
とりあえず今回からはmmsystemを宣言している。
Cならばlibのインクルードか動的ロードが必要だ。
そして変数としてSystypesで宣言しているInt64を兼ねたArec64を使う。
testproc:Tproc;
procs=array[0..7] of pointer;
tst_stt,tst_end,tst_rs:Arec64;
cnt_mes,cnt_stt,cnt_end,cnt_rs,cnt_prm,cnt_pre:Arec64;
s_time:SystemTime;
等を宣言し
肝となる
Procedure get_tsc;
asm
cpuid
rdtsc
Mov tst_stt.l,EAX
Mov tst_stt.h,EDX
call testproc
cpuid
rdtsc
Mov tst_end.l,EAX
Mov tst_end.h,EDX
end;
を定義。
要するにtestprocに代入された関数の前後でクロックを取得している。
WinAPIを呼ぶ場合はスレッドを切られる可能性があるので時間は多少ばらつく。
testprocに代入する関数を変更することでターゲットを変更する。
実験用の変数群
procedure def_TProc;
begin
end;
procedure tick_Proc;
begin
cnt_mes.l:=GetTickCount;
end;
procedure time_Proc;
begin
cnt_mes.l:=timeGetTime;
end;
procedure systime_Proc;
begin
GetSystemTime(s_time);
end;
procedure rdtsc_Proc;
asm
cpuid
rdtsc
mov cnt_mes.l,EAX
mov cnt_mes.h,EDX
end;
procedure qtime_Proc;
begin
QueryPerformanceCounter(cnt_mes.w);
end;
procedure sleep_Proc;
begin
sleep(0);
end;
等を宣言
初期化時に
testproc:=@def_TProc;
DoProc:=@info_Proc;
procs1[0]:=@def_TProc;
procs1[1]:=@tick_Proc;
procs1[2]:=@time_Proc;
procs1[3]:=@systime_Proc;
procs1[4]:=@rdtsc_Proc;
procs1[5]:=@qtime_Proc;
procs1[6]:=@sleep_Proc;
procs1[7]:=@def_TProc;
とする。
次に今回の表示は普通にメッセージボックスを使って
procedure info_Proc;
begin
get_tsc;
tst_rs.w:=tst_end.w-tst_stt.w;
Winprintf(@mes[0],'%d;',tst_rs.l);
Infomation(@mes[0]);
end;
としたがあくまでstringを使わないという方針でやっているだけなので
普通にdelphiで書く場合は
var
l_str1:string;
str(tst_rs.w,l_str1);
Infomation(@l_str1[1]);
とした方が64bit普通に表示できる。
この差は21.5kbyteか27kbyteかの違いで
むしろstringとdelphiデフォルトのメモリー管理の使用の有無と移植性だけの問題だ。
ここまでで気になったこととしてはget_tscの後に
グローバル変数を使うWinprintf(Wsprintf)を2回呼び出したところエラーにならなかったが
4回呼び出して位置を調整させたらアプリケーションエラーが出た。
get_tscをはずすとエラーは出ないのでオーダリング関係の問題だと思う。
あとメニュー処理としては
$6000..$6FFF:begin bmp1(wk_wp-$6000);end;
bmp1の中で
$10..$17:testproc:=procs1[l_param-$10];
メニュー生成字に
AppendMenu(Sub,0,$6010,'func');//
AppendMenu(Sub,0,$6011,'tickcount');//
AppendMenu(Sub,0,$6012,'timegettime');//
AppendMenu(Sub,0,$6013,'systime');//
AppendMenu(Sub,0,$6014,'rdtsc');//
AppendMenu(Sub,0,$6015,'QueryPerformanceCounter');//
AppendMenu(Sub,0,$6016,'sleep');//
ファイルサイズどうこう言っている割になんかこの辺はずるずる増殖させている。
ほんとうは文字列リソースと関数で管理した方がよい。
で結果だが
まず前提としてtimeBeginPeriod(1)を使っているのだが
宣言しなくても負荷が軽くなることは無い。
- 関数を呼び出すだけの場合 :580-592クロック(他の命令を実行した後だと変化する。)
- GettickCount :1900-3992クロック
- TimeGetTime :1回目は30000-46000クロック、2回目以降は1800-2400クロック
- getSystemTime :1回目は11000クロック、2回目以降は3000-8000クロック
- QueryPerformanceCounter :1回目は55680-55750クロック、2回目以降は15000-15880クロック
- QueryPerformanceCounter :事前にQueryPerformanceFrequencyつきだと、9000-10000クロック
- rdtsc(そのまま) :1092-1600クロック
- rdtsc(cpuid無し) :824または884クロックたまに632クロック
- rdtsc(cpuidあり) :1000-1600クロック
- sleep(0) :7700-10760クロック
- sleep(1) :1761700-1792000クロック
- sleep(10) :17989500-18005054クロック
- sleep(1000) :1800220188-180023452クロック
値は関数を呼び出すだけの場合の値を引いていない。
sleep(0)は実行時間というよりは他のスレッドをグルっとまわった値と見た方がよい。
ちなみにQueryPerformanceFrequency(cnt_prm.w)で値は1193182固定だった。
つまり単位上のQueryPerformanceCounterの最高精度は私のPCの場合1μsで
32bitでの上限は3599sつまり1時間弱だということになるので64bit処理は必須のようだ。
gettickcountの安定性とTimeGetTimeの負荷が意外と低いのが目につくと同時に
システム時計へのアクセスであるgetSystemTimeがそんなには重くないのが特徴ではないかと思う。
これについては保存パラメータを使うなどの最適化がOSでなされている可能性もあるが
例えば1秒に1回絶対時間の取得するところを節約して
起動時にはかりタイマー関数を加算したりする手法はあまり意味が無いといえそうだ。
又、TimeGetTimeとQueryPerformanceCounterの2命令は1回目に負荷が高い。
多くの実験サイトでTimeGetTimeとQueryPerformanceCounterの負荷を重い目に報告しているようだが
これは一発目、ないしは一発目を含む数回を測定しているからではないかと思われる。
但しQueryPerformanceFrequencyを事前に呼んでいる場合は最初から同じような値になる。
次の実験で精度に関する話を載せるが負荷の面でTimeGetTimeがgettickcountと遜色ないというのが
最も大きな結果であり通常の時間測定では安心してTimeGetTimeを使ってよいというのが今回の結論だとも言える。
参考のためget_tscのtestprocを呼んでいるところを直接cpuidとrdtscを代入しても1052クロックだったから
関数呼び出しと比較して大して減っていないことからdelphi固有の問題は意外とないのかなと思えた。
多分P3だともう少しクロックをくい。新しいものだとクロックはより少ないのだろうと思われる。
追記
TimegetTimeとTimegetSystemTimeとどちらが素か知りたかったので
TimegetSystemTimeも計測したところだいたい3500-4000クロック前後となり
2000前後でおさまるTimegetTimeの方が基本だという事を確認した。
Window98では結局ここいら全部遅いみたいなのでGetTickCountの方が良いということのようだ。
ラベル:RDTSC