正確な時間枬定、適甚

この蚘事の目的は 、可胜な限り正確に時間を枬定する方法に関する問題の䜜業䞭に埗られた資料を提瀺し、これらの方法を実際に䜿甚するこずず、達成可胜な最高の粟床で゜フトりェアを管理するためのオプションを怜蚎するこずです。

この蚘事は 、すでにプログラミングの経隓があり、暙準機胜の時間間隔の露出の粟床の問題に気づく読者を察象ずしおいたす。 すべおのメ゜ッドはこの蚀語で実装されおいるため、蚘事の著者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;


結局、 SetWaitableTimerずCloseHandleも、考慮しおいるプロセッサヌクロック数の期間実行されたす。 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ず議論したした。

Source: https://habr.com/ru/post/J75234/


All Articles