正確な時間:測定、適用

この記事の目的は 、可能な限り正確に時間を測定する方法に関する問題の作業中に得られた資料を提示し、これらの方法を実際に使用することと、達成可能な最高の精度でソフトウェアを管理するためのオプションを検討することです。



この記事は 、すでにプログラミングの経験があり、標準機能の時間間隔の露出の精度の問題に気づく読者を対象としています。 すべてのメソッドはこの言語で実装されているため、記事の著者begin_endは 、Delphiでプログラミングする読者にアドバイスしています。



私たちのタスクは、短い時間間隔(望ましい精度は10 ^ -6秒)の正確な測定のための最良の方法を見つけ、同じ精度でコードの実行における遅延のプログラミングの最も効果的な方法を決定することです。



さまざまなアプリケーションアプリケーション(たとえば、データ送信または信号生成/分析に関連するアプリケーション)を既に開発しようとしたプログラマーは、時間間隔の値が小さい場合、すべての標準機能( スリープ、ビープ音、GetTickCount 、タイマー)に大きなエラーがあることに気付くことがあります。 これは、システムタイマーの解像度によって決まります。システムタイマーの値は、コンピューターによって多少異なる場合があります。 GetSystemTimeAdjustment関数を使用して、この許可を確認できます。



BOOL GetSystemTimeAdjustment(

PDWORD lpTimeAdjustment, // size, in 100-nanosecond units, of a periodic time adjustment

PDWORD lpTimeIncrement, // time, in 100-nanosecond units, between periodic time adjustments

PBOOL lpTimeAdjustmentDisabled // whether periodic time adjustment is disabled or enabled

);








Delphiで使用するためにこの関数を分析しましょう。 LpTimeIncrementは、システムタイマーの解像度を100ナノ秒単位で記録します。 この値を取得して、たとえばミリ秒単位で出力する必要があります。 これにより、このようなプログラムが作成されます( 例1を参照 )。



program SysTmrCycle;



{$APPTYPE CONSOLE}



uses

SysUtils, windows;



var a,b:DWORD; c:bool;

begin

GetSystemTimeAdjustment(a,b,c);

WriteLn('System time adjustment: '+FloatToStr(b / 10000)+' ms.');

WriteLn;

Writeln('Press any key for an exit...');

Readln;

end.








実行結果が画面に表示され、タイマー値は10.0144ミリ秒であることが判明しました。



この値は本当にどういう意味ですか? 関数の時間間隔がほぼ常にこの値の倍数になるという事実。 10.0144ミリ秒の場合、 スリープ(1000)機能により1001.44ミリ秒の遅延が発生します。 sleep(5)を呼び出すと、遅延は約10ミリ秒になります。 標準のDelphiタイマーであるTTimerオブジェクトは、自然にエラーを起こしやすい傾向がありますが、その程度はさらに大きくなります。 TTimerオブジェクトは、通常のWindowsタイマーに基づいており、ウィンドウと非同期ではないWM_TIMERメッセージを送信します。 これらのメッセージは、他のすべてのメッセージと同様に、アプリケーションの通常のメッセージキューに入れられて処理されます。 さらに、WM_TIMERは、他のメッセージと比較して最も低い優先度(WM_PAINTを除く)を持っています。 GetMessageは、キューに優先度メッセージがなくなった場合にのみ処理のためにWM_TIMERメッセージを送信します-WM_TIMERメッセージはかなりの時間遅延する可能性があります。 遅延時間が間隔を超えると、メッセージが結合されるため、損失も発生します[1]。

少なくとも何らかの方法で遅延関数の比較分析を測定するには、コードの特定のセクションの実行の時間間隔を正確に測定できるツールが必要です。 上記のため、 GetTickCountは機能しません。 しかし、著者は、特定の時間間隔でプロセッサのクロック周波数に依存する能力について知りました。 Pentium III以降では、プロセッサには通常、プログラマのリアルタイムカウンターカウンター、タイムスタンプカウンター、 TSCが含まれています。これは、クロックサイクルごとに内容がインクリメントされる64ビットのレジスタです[2]。 カウンターのカウントは、コンピューターの起動(またはハードウェアリセット)のたびにゼロから始まります。 Delphiで次のようにカウンタ値を取得できます( 例2を参照 )。



program rdtsc_view;



{$APPTYPE CONSOLE}



uses

SysUtils, windows;



function tsc: Int64;

var ts: record

case byte of

1: (count: Int64);

2: (b, a: cardinal);

end;

begin

asm

db $F;

db $31;

mov [ts.a], edx

mov [ts.b], eax

end;

tsc:=ts.count;

end;



begin

repeat WriteLn(FloatToStr(tsc)) until false;

end.








ここで、アセンブラーの挿入はカウンター結果をedxおよびeaxレジスターに入れ、その値はtsに転送され、そこからInt64型のts.countとして使用できます。 上記のプログラムは、コンソールにカウンター値を継続的に表示します。 Delphiの一部のバージョンには、次のようなRDTSC関数[ 3 ] を使用してカウンター値すぐに取得できる既製のrdtsc (タイムスタンプカウンターの読み取り)コマンドがあります。



function RDTSC: Int64; register;

asm

rdtsc

end;








カウンター値があると仮定しますが、その使用方法は? とても簡単です。 値が一定の頻度で変化するという事実に基づいて、調査中のコマンドの後とその前のプロセッササイクル数の差を計算できます。



a:=tsc;

Command;

b:=tsc-a;








bには、コマンドの実行中に経過したプロセッササイクルの数が入ります。 しかし、1つのポイントがあります。 メジャーの数を提供するtsc呼び出しも、これにいくつかのメジャーを費やす必要があります。 そして、結果の忠実度のために、得られたメジャーの数から差し引かれた補正として導入されなければなりません:



a:=tsc;

C:=tsc-a;

a:=tsc;

Command;

b:=tsc-aC;








すべてうまくいきますが、実験的には、補正Cの値が異なることがあります。 この理由は見つかりました。 ここでのポイントは、特にプロセッサーの機能、またはむしろそのコンベヤーです。 コンベアに沿った機械命令の前進は、いくつかの基本的な困難に関連しています。それぞれの場合、コンベアはアイドル状態です。 命令の実行時間は、せいぜいパイプラインのスループットによって決まります。 プロセッササイクルを受信して​​保証されることが保証される時間間隔-50サイクルから[2]。 補正が決定された場合、最も正確な値は最小値になることがわかります。 実験的に、修正関数を最大10回呼び出すだけで十分です。



function calibrate_runtime:Int64;

var i:byte; tstsc,tetsc,crtm:Int64;

begin

tstsc:=tsc;

crtm:=tsc-tstsc;

for i:=0 to 9 do

begin

tstsc:=tsc;

crtm:=tsc-tstsc;

if tetsc<crtm then crtm:=tetsc;

end;

calibrate_runtime:=crtm;

end;








必要なツールが揃ったので、遅延関数を試してみましょう。 よく知られ広く使用されている睡眠から始めましょう:



procedure Sleep(milliseconds: Cardinal); stdcall;







遅延の正確さを確認するために、コンソールプログラムに、 TSCコードとキャリブレーション ランタイムコードに加えて、次のコードを含めます。



function cycleperms(pau_dur:cardinal):Int64;

var tstsc,tetsc:Int64;

begin

tstsc:=tsc;

sleep(pau_dur);

tetsc:=tsc-tstsc;

cycleperms:=(tetsc-calibrate_runtime) div pau_dur;

end;








プログラムからこのコードを呼び出して、pau_durの値(ポーズ)を数回設定します気づいたら、ポーズ中の小節数をポーズ値で割ってください。 そのため、時間に応じて遅延の精度がわかります。 テストを実行し、テスト結果を表示/保存するために、次のコードが使用されました( 例3を参照 )。



var test_result,temp_result:string; n:cardinal; i:byte; aver,t_res:Int64; res:TextFile;

begin

WriteLn('The program will generate a file containing the table of results of measurements of quantity of cycles of the processor in a millisecond. Time of measurement is chosen'+' miscellaneous, intervals: 1, 10, 100, 1000, 10000 ms. You will see distinctions of measurements. If an interval of measurement longer - results will be more exact.');

WriteLn;

Writeln('Expected time of check - 1 minute. Press any key for start of the test...');

ReadLn;

temp_result:='Delay :'+#9+'Test 1:'+#9+'Test 2:'+#9+'Test 3:'+#9+'Test 4:'+#9+'Test 5:'+#9+'Average:';

n:=1;

test_result:=temp_result;

WriteLn(test_result);

while n<=10000 do

begin

temp_result:=IntToStr(n)+'ms'+#9;

aver:=0;

for i:=1 to 5 do

begin

t_res:=cycleperms(n);

aver:=aver+t_res;

temp_result:=temp_result+IntToStr(t_res)+#9;

end;

WriteLn(temp_result+IntToStr(aver div 5));

test_result:=test_result+#13+#10+temp_result+IntToStr(aver div 5);

n:=n*10;

end;

WriteLn;

AssignFile(res,'TCC_DEF.xls');

ReWrite(res);

Write(res,test_result);

CloseFile(res);

WriteLn('The test is completed. The data are saved in a file TCC_DEF.xls.');

Writeln('Press any key for an exit...');

ReadLn;

end.








その中で、各時間間隔( 1〜10,000ミリ秒)でcyclepermsを 5回実行し、平均値も考慮します。 テーブルが判明しました。 そのため、そのような調査中に取得されたプロセッサクロックの数:

TCC_DEF



私たちが観察している写真は最高ではありません。 プロセッサの周波数は約1778.8 MHz( 例4を参照 )であるため、1ミリ秒のクロック値は約1778800の数値を目指す必要があります。 スリープ機能の精度は、1、10、100、または1000ミリ秒の間これを与えません。 値が近いのは、10秒間だけです。 おそらく、テスト4で1781146がなかった場合、平均値は許容範囲になります。

何ができますか? 関数を離れて、何か他のものを検討しますか? まだ急いではいけません。 timeBeginPeriod [2]関数を使用して、基準時間間隔のエラーを手動で設定できることがわかりました。



MMRESULT timeBeginPeriod(

UINT uPeriod

);








この高精度の解像度を維持するには、追加のシステムリソースが使用されるため、すべての操作が完了した後にtimeEndPeriodを呼び出してそれらを解放する必要があります。 そのような睡眠を調査するためのcycleperms関数のコード( 例5を参照 ):



function cycleperms(pau_dur:cardinal):Int64;

var tstsc,tetsc:Int64;

begin

timeBeginPeriod(1);

sleep(10);

tstsc:=tsc;

sleep(pau_dur);

tetsc:=tsc-tstsc;

timeEndPeriod(1);

cycleperms:=(tetsc-calibrate_runtime) div pau_dur;

end;








解決不可能な機能であるtimeBeginPeriod(1)は、解像度を1ミリ秒に設定しますが、すぐに効果が現れなくなりますが、 スリープコールの後でのみです。そのため、 timeBeginPeriodの後にコードにsleep(10)が挿入されます。 この研究の結果:

Tcc



観測されたデータははるかに優れています。 10秒にわたる平均はかなり正確です。 1ミリ秒の平均は1.7%だけ異なります。 したがって、10ミリ秒の差は0.056%、100ミリ秒-0.33%(ストレンジが発生)、1000ミリ秒-0.01%です。 1 msより短い間隔は、 スリープでは使用できません。 ただし、 timeBeginPeriod(1)が実行され、指定された時間間隔が長くなるとスリープの精度が上がるだけで、 スリープは 1ミリ秒の休止に適していると断言できます( 例6を参照 )。



スリープ関数は、 NtDelayExecution関数のネイティブAPIに基づいており、次の形式があります[ 5 ]。



NtDelayExecution(

IN BOOLEAN Alertable,

IN PLARGE_INTEGER DelayInterval );








sleepのような遅延をテストしてみましょうが、マイクロ秒でもそれを考慮に入れます:



function cyclepermks(pau_dur:Int64):Int64;

var tstsc,tetsc,p:Int64;

begin

p:=-10*pau_dur;

tstsc:=tsc;

NtDelayExecution(false,@p);

tetsc:=tsc-tstsc;

cyclepermks:=(tetsc-calibrate_runtime) *1000 div pau_dur;

end;








この関数はwindows.pasやその他のファイルに登録されていないため、次の行を追加して呼び出します。



procedure NtDelayExecution(Alertable:boolean;Interval:PInt64); stdcall; external 'ntdll.dll';







関数を呼び出して結果のテーブルを作成するコードは、次のように修正する必要があります( 例7を参照 )。



var test_result,temp_result:string; n:Int64; i:byte; aver,t_res:Int64; res:TextFile;

begin

WriteLn('The program will generate a file containing the table of results of measurements of quantity of cycles of the processor in a mikrosecond. Time of measurement is chosen'+' miscellaneous, intervals: 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000 mks. You will see distinctions of measurements. If an interval of measurement longer - results will be more exact.');

WriteLn;

Writeln('Expected time of check - 1 minute. Press any key for start of the test...');

temp_result:='Delay :'+#9+'Test 1:'+#9+'Test 2:'+#9+'Test 3:'+#9+'Test 4:'+#9+'Test 5:'+#9+'Average:';

n:=1;

test_result:=temp_result;

WriteLn(test_result);

while n<=10000000 do

begin

temp_result:='10^'+IntToStr(length(IntToStr(n))-1)+'mks'+#9;

aver:=0;

for i:=1 to 5 do

begin

t_res:=cyclepermks(n);

aver:=aver+t_res;

temp_result:=temp_result+IntToStr(t_res)+#9;

end;

WriteLn(temp_result+IntToStr(aver div 5));

test_result:=test_result+#13+#10+temp_result+IntToStr(aver div 5);

n:=n*10;

end;

WriteLn;

AssignFile(res,'TCC_NTAPI.xls');

ReWrite(res);

Write(res,test_result);

CloseFile(res);

WriteLn('The test is completed. The data are saved in a file TCC_NTAPI.xls.');

Writeln('Press any key for an exit...');

ReadLn;

end.








NtDelayExecutionによって作成された遅延の調査を実施した後、興味深い結果が得られました。

TCC_NTAPI



1ミリ秒未満の間隔でこの関数にこのような精度を適用することは無意味であることがわかります。 他の遅延間隔は、解像度を変更せずにスリープするよりもわずかに優れていますが、高解像度でスリープするよりも悪いです(これは原則として理解できます。なぜなら、ここでは優先度の高いフローを作成せず、これによりtimeBeginPeriodになります )。 そして、 timeBeginPeriodを追加する ? 何が起こるか見てみましょう:

NTAPI2



マイクロ秒間隔で、状況は同じです。 ただし、1ミリ秒から始まる間隔では、10秒の値との差は0.84%で、これはスリープの同様の使用(1.7%)よりも優れています-NtDelayExecutionはより正確に遅延を与えます。

コード実行の遅延についてプログラミングツールを検索すると、別のオプションが見つかりました[ 4 ]。これは、マイクロ秒単位で間隔を指定する機能を提供しているようです。 これはWaitableTimerです。 CreateWaitableTimer関数、SetWaitableTimer関数、WaitForSingleObjectEx関数を使用して操作できます。 WaitableTimerを追加したcyclepermksプロシージャのビュー:



function cyclepermks(pau_dur:Int64):Int64;

var tstsc,tetsc,p:Int64; tmr:cardinal;

begin

tmr:=CreateWaitableTimer(nil, false, nil);

p:=-10*pau_dur;

tstsc:=tsc;

SetWaitableTimer(tmr, p, 0, nil, nil, false);

WaitForSingleObjectEx(tmr, infinite, true);

CloseHandle(tmr);

tetsc:=tsc-tstsc;

cyclepermks:=(tetsc-calibrate_runtime2) *1000 div pau_dur;

end;








WaitableTimerを使用することの特性により、 calibrate_runtimeで取得した補正の計算を変更する必要あります



function calibrate_runtime2:Int64;

var i:byte; tstsc,tetsc,crtm, p:Int64; tmr:cardinal;

begin

tstsc:=tsc;

crtm:=tsc-tstsc;

for i:=0 to 9 do

begin

tmr:=CreateWaitableTimer(nil, false, nil);

p:=0;

tstsc:=tsc;

SetWaitableTimer(tmr, p, 0, nil, nil, false);

CloseHandle(tmr);

crtm:=tsc-tstsc;

if tetsc<crtm then crtm:=tetsc;

end;

calibrate_runtime2:=crtm;

end;








結局、 SetWaitableTimerCloseHandleも、考慮しているプロセッサークロック数の期間実行されます。 timeBeginPeriod呼び出しをcyclepermks コードにすぐに追加し、この手順が精度の向上に役立つことを期待しています( 例8を参照 )。 結果表:

TCC_WFSO



残念ながら、ここではミリ秒未満の間隔に遅延を設定する機会がありませんでした。 1ミリ秒と10秒の値の差は5%です。 以前の方法と比較して、これはさらに悪いです。

結論を出す前に、時間そのものの実際の測定について少し説明します。 上記の研究では、比較の基礎はプロセッササイクルの数であり、各コンピュータには異なるものがあります。 秒に基づいて時間の単位に変換する必要がある場合は、次を実行する必要があります: NtDelayExecution 10-second delayを使用して、これらの10秒間のプロセッササイクル数を取得するか、1サイクルの期間を確認します( 例9を参照 )。 単位時間あたりのプロセッササイクル数がわかっている場合、プロセッササイクル数の小さい値を時間値に安全に変換できます。 さらに、アプリケーションのリアルタイム優先度を設定することをお勧めします。



おわりに 実行された作業の結果、コンピューターで非常に正確に時間を測定できることがわかりました(50プロセッサーサイクルで推定される時間まで)。 この問題は正常に解決されました。 実行可能コードの正確な遅延を個別に設定する機能に関しては、状況は次のとおりです:検出された最良の方法により、1ミリ秒以下の解像度でこれを行うことができ、1 ms間隔で約0.84%の解像度エラーが発生します。 これは、 timeBeginIntervalプロシージャによるアクセス許可を設定するNtDelayExecution関数です。 この関数の欠点は、精度の低いスリープと比較して、面倒な呼び出しとドキュメント化されていないネイティブAPIの存在です。 ネイティブAPIの使用は、Windowsファミリの異なるオペレーティングシステムで個々のAPIの非互換性が生じる可能性があるため、お勧めしません。 一般に、 NtDelayExecution関数の明らかな利点は、 依然として有利な選択を強制します。



例:

1. システムタイマーの解像度の決定

2. RDTSC出力

3. スリープまでの間隔を設定します

4. プロセッサの周波数を調べる

5. スリープの間隔をより正確に設定する

6. スリープ中の間隔を異なる値に設定する精度を調査します

7. NtDelayExecutionを使用した間隔

8. WaitableTimerを使用した間隔

9. 1プロセッササイクルの期間を調べる

例には、ソースコード* .dprファイル(Delphiの場合)、コンパイルされたコンソール* .exeアプリケーション、および(MS Excelでサポートされている形式の)作成者が既に取得した結果の* .xlsテーブルが含まれます。 すべての例は1つのファイルに収められています



参照:

1. Russinovich M.、Solomon D.内部デバイスMicrosoft Windows。 -サンクトペテルブルク:ピーター、2005年-992 p。

2. Schupak Yu.A. Win32 API。 効果的なアプリケーション開発。 -サンクトペテルブルク:ピーター、2007年-572 p。

3. RDTSC-ウィキペディア[ http://ru.wikipedia.org/wiki/Rdtsc ]

4. CreateWaitableTimer-MSDN [ http://msdn.microsoft.com/en-us/library/ms682492(VS.85).aspx ]

5. NtDelayExecution-RealCoding [ http://forums.realcoding.net/lofiversion/index.php/t16146.html ]



この記事は2009年 11月13日にbegin_endによって書かれました 。 著者は、記事で考慮されたいくつかのポイントをsleshと議論しました。




All Articles