The purpose of this article is to state the material obtained in the course of work on a problem on how to measure time as accurately as possible and how to use these methods in practice, and also to consider options for managing something programmed with the maximum achievable accuracy.
The article is intended for readers who already have some experience in programming and who have noticed the problem of the accuracy of the exposure of time intervals of standard functions. The author of the article,
begin_end , advises its readers programming in the Delphi language, since all methods are implemented in this language.
Our task is to find the best method for accurate measurement of small time intervals (the desired accuracy is 10 ^ -6 seconds), to determine the most effective way of programming delays in the execution of a code, with the same accuracy.
')
A programmer who has already tried to develop various applications, for example, related to data transfer or signal generation / analysis, could notice that all standard functions (
sleep, beep, GetTickCount , timers) have a large error when working with small time intervals.
This is determined by the resolution of the system timer, the value of which for different computers may vary somewhat. You can learn this permission using the GetSystemTimeAdjustment function:
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
);
Let's sort this function for use in Delphi. LpTimeIncrement records the resolution value of the system timer in units of 100 nanoseconds. We need to get this value, and output it, for example, in milliseconds. Such program will turn out (
see example 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.
The result of the execution is displayed on the screen, my timer value was equal to 10,0144 milliseconds.
What does this quantity really mean? The fact that the time intervals of the functions will almost always be multiples of this value. If it is 10,0144 ms, then the
sleep function
(1000) will cause a delay of 1001.44 ms. When calling
sleep (5), the delay will be approximately 10 ms. The standard Delphi timer, a TTimer object, is naturally prone to error, but to an even greater degree. The TTimer object is based on the regular Windows timer, and sends WM_TIMER messages to the window that are not asynchronous. These messages are placed in the normal message queue of the application and are processed, like all the others. In addition, WM_TIMER has the lowest priority (excluding WM_PAINT), relative to other messages.
GetMessage sends a WM_TIMER message for processing only when the priority messages in the queue no longer remain - WM_TIMER messages may be delayed for a considerable time. If the delay time exceeds the interval, the messages are combined together, thus, there is also their loss [1].
In order to at least make measurements for a comparative analysis of the delay functions, a tool is needed to accurately measure the time intervals for the execution of a certain code segment.
GetTickCount will not work in view of the above. But the author learned about the possibility of relying on the processor clock frequency, for a certain time interval. Starting with the Pentium III, the processors usually contain a real-time tag counter, available to programmers, the Time Stamp Counter,
TSC , which is a register of 64 bits, the contents of which are incremented with each processor tick [2]. The counter counting starts from zero each time the computer starts (or hardware resets). You can get the value of the counter in Delphi as follows (
see example 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.
Here the assembler insertion places the result of the counter into the
edx and
eax registers, the value of which is then transferred to ts, from where it is available as ts.count of type Int64. The above program continuously displays the counter values in the console. On some versions of Delphi there is a ready-made
rdtsc command (read time stamp counter), which allows you to immediately get the counter value by the
RDTSC function [
3 ] like this:
function RDTSC: Int64; register;
asm
rdtsc
end;
Suppose we have a counter value, but how to use it? Very simple. Based on the fact that the value changes with a constant frequency, it is possible to calculate the difference in the number of processor cycles after the command being examined and before it:
a:=tsc;
Command;
b:=tsc-a;
In b there will be the number of processor ticks elapsed during the execution of the Command. But there is one moment. The
tsc call, which gives us the number of
ticks , must itself also spend on it a certain number of ticks. And, for the accuracy of the result, it must be made, as an amendment, subtracted from the received number of measures:
a:=tsc;
C:=tsc-a;
a:=tsc;
Command;
b:=tsc-aC;
Everything would be fine, but experimentally it turns out that sometimes the values of our C amendment differ. The reason for this was found. The point here is in particular the functioning of the processor, or rather its pipeline. Promotion of machine instructions for the conveyor is associated with a number of fundamental difficulties, in the case of each of them the conveyor is idle. The execution time of the instruction is at best determined by the throughput of the pipeline. The time interval, which can be guaranteed to believe, getting processor clock cycles - from 50 clock cycles [2]. It turns out that in the case of determining the amendment, the most accurate value will be the minimum value. Experimentally, it is enough to call the correction function up to 10 times:
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;
Now that we have the necessary tool, let's experiment with the delay functions. Let's start with the well-known and all applicable
sleep :
procedure Sleep(milliseconds: Cardinal); stdcall;
To check the accuracy of the delay, we include in our console program, in addition to the
tsc code and the
calibrate_runtime code, the following code:
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;
We will call this code from the program, setting different pau_dur values (pauses) several times. If you noticed, the number of ticks during the pause is then divided by the pause value. So we will know the accuracy of the delay depending on her time. For the convenience of conducting the test and displaying / saving the result of the test, the following code is used (
see Example 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.
In it, we execute
cycleperms five times for each time interval (from 1 to 10,000 milliseconds), and also consider the average value. It turns out the table. So, the processor clock numbers obtained during this study:

The picture we see is not the best. Since the processor frequency is approximately 1778.8 MHz (
see Example 4 ), the 1-millisecond clock values should tend to an approximate number of 1778800. The accuracy of the
sleep function does not give us that in 1, 10, 100, or 1000 milliseconds. Only for a ten-second period of time values are close. Perhaps, if test 4 were not 1781146, then the average value would be acceptable.
What can be done? Leave the function and consider something else? Do not hurry yet. I learned that you can manually set the reference error of the reference time interval using the
timeBeginPeriod function [2]:
MMRESULT timeBeginPeriod(
UINT uPeriod
);
To maintain this high-resolution resolution, additional system resources are used, so you need to call
timeEndPeriod to release them upon completion of all operations. Function code cycleperms for the study of such
sleep (
see example 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;
There is also an unexplainable feature,
timeBeginPeriod (1) , which sets the resolution to 1 millisecond and does not begin to take effect immediately, but only after calling
sleep , therefore
sleep (10) is inserted into the code after
timeBeginPeriod . The results of this study:

The observed data is much better. The average value for 10 seconds is pretty accurate. The average for 1 millisecond differs from it only by 1.7%. Accordingly, the difference for 10 ms is 0.056%, for 100 ms - 0.33% (strangely), for 1000 ms - 0.01%. Smaller than 1 ms interval, cannot be used in
sleep . But it can be firmly said that
sleep is suitable for pauses of 1 ms under the condition that
timeBeginPeriod (1) is executed, and the accuracy of
sleep only increases with the growth of the specified time interval (
see Example 6 ).
The
sleep function is based on the Native API of the
NtDelayExecution function, which has the following form [
5 ]:
NtDelayExecution(
IN BOOLEAN Alertable,
IN PLARGE_INTEGER DelayInterval );
Let's try to test its delays, like
sleep , but it will take into account even microseconds:
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;
This function is not registered in windows.pas or another file, therefore we will call it by adding a line:
procedure NtDelayExecution(Alertable:boolean;Interval:PInt64); stdcall; external 'ntdll.dll';
The code in which we call a function and build a table of results should be corrected like this (
see Example 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.
After conducting a study of the delays created by
NtDelayExecution , interesting results were obtained:

It can be seen that it is useless to apply such accuracy on intervals of less than 1 millisecond. Other delay intervals are somewhat better than
sleep without a modified resolution, but worse than with high resolution sleep (in principle, this is understandable, because here we did not create threads with elevated priority, and did not do anything at all to improve accuracy, just like this makes
timeBeginPeriod ). And if you add
timeBeginPeriod ? Let's see what happens:

At microsecond intervals, the situation is the same. But at intervals starting from 1 millisecond the difference with respect to the 10-second value is 0.84%, which is better than the similar use of sleep (1.7%) -
NtDelayExecution gives a more accurate delay.
When searching for means of programming delays in the execution of the code, another option was found [
4 ], which seems to provide the ability to specify the interval in microseconds. This is a
WaitableTimer . You can work with it through the
CreateWaitableTimer, SetWaitableTimer, WaitForSingleObjectEx functions. Type of procedure
cyclepermks , where we added
WaitableTimer :
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;
The peculiarity of the use of the
WaitableTimer also requires us to modify the calculation of the correction obtained in the
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;
After all,
SetWaitableTimer and
CloseHandle are also executed for the period of the number of processor clock counts we take into account. Immediately add a call to
timeBeginPeriod to the cyclepermks code , hoping for the help of this procedure in increasing accuracy (
see Example 8 ). Result table:

Alas, here too we did not get the opportunity to set delays for gaps less than millisecond. The difference of 1 millisecond and 10 seconds is 5%. In comparison with the previous methods, it is worse.
Before drawing conclusions, I will say a little about the actual measurement of time itself. In the above studies, the basis of comparisons was the number of processor cycles and each computer has a different one. If you need to bring it to units of time based on seconds, you need to do the following: using the 10-second delay of
NtDelayExecution, get the number of processor cycles in these 10 seconds or find out the duration of one cycle (
see example 9 ). Knowing the number of processor cycles per unit of time, you can easily convert smaller values of the number of processor cycles into time values. In addition, it is recommended to set the application to real-time priority.
Conclusion As a result of this work, it was found that it is possible to measure time on a computer very accurately (even up to a length of time, calculated at 50 processor cycles). This problem was solved successfully. As for the ability to independently set the exact delays in the executable code, the situation is as follows: the best method found allows you to do this with a resolution of no more than 1 millisecond, with an error of resolution in the 1 ms interval of about 0.84%. This is a
NtDelayExecution function with the permission set by the
timeBeginInterval procedure. The disadvantage of the function, compared to the less accurate sleep, is a cumbersome call and being part of an insufficiently documented Native API. The use of the Native API is not advised because of the possible incompatibility of separate APIs in different operating systems of the Windows family. In general, the obvious advantage of the
NtDelayExecution function still forces you to make a choice in its favor.
Examples:1.
Determine the resolution of the system timer2.
RDTSC output3.
Set the interval through sleep4.
Find out the frequency of the processor5.
Set the interval through sleep more accurately6.
We investigate the accuracy of setting the interval through sleep at different values.7.
Interval with NtDelayExecution8.
Interval by WaitableTimer9.
Recognize the duration of one processor cycleThe examples include source code * .dpr files (in Delphi language), a compiled console * .exe application and (some) * .xls table of results already received by the author (in a format supported by MS Excel). All examples are one file .Literature:1. Russinovich M., Solomon D. The internal structure of Microsoft Windows. - SPb .: Peter, 2005. - 992 p.
2. Shchupak Yu.A. Win32 API. Effective application development. - SPb .: Peter, 2007. - 572 p.
3. RDTSC - Wikipedia [
http://en.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 ]
The article was written on November 13, 2009, by
begin_end . The author discussed some points considered in the article with
slesh ', to whom gratitude for such assistance is expressed.