Exact time: measure, apply

       The purpose of this article is to present the material obtained in the course of work on the problem on how to measure time as accurately as possible and to use these methods in practice, as well as to consider options for managing something software with the highest achievable accuracy.

       The article is intended for readers who already have some experience in programming and who notice the problem of accuracy of exposure of time intervals of standard functions. The author of the article, begin_end , advises its readers programming in Delphi, since all methods are implemented in this language.

       Our challenge- find the best method for accurate measurement of small time intervals (the desired accuracy is 10 ^ -6 seconds), determine the most effective way of programming delays in the execution of the code, with the same accuracy.

       A programmer who has already tried to develop various application applications, for example, those related to data transmission or signal generation / analysis, could notice that all standard functions ( sleep, beep, GetTickCount , timers) have a large error when working with small values ​​of the time interval. This is determined by the resolution of the system timer, the value of which for different computers may vary slightly. You can find out 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
       );


       We will analyze this function for use in Delphi. LpTimeIncrement records the resolution of the system timer in units of 100 nanoseconds. We need to get this value, and output it, for example, in milliseconds. This will result in such a program ( see example 1 ):    The result of the execution is displayed on the screen, my timer value turned out to be 10.0144 milliseconds.    What does this value really mean? The fact that the time intervals of functions will almost always be multiples of this value. If it is 10.0144 ms, then the sleep (1000) function will cause a delay of 1001.44 ms. When you call sleep (5)

    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 delay will be approximately 10 ms. The standard Delphi timer, the TTimer object, is naturally prone to error, but to an even greater extent. The TTimer object is based on a regular Windows timer and sends WM_TIMER messages that are not asynchronous to the window. These messages are put into the normal message queue of the application and are processed, like all others. In addition, WM_TIMER has the lowest priority (excluding WM_PAINT) in relation to other messages. GetMessage sends a WM_TIMER message for processing only when there are no more priority messages in the queue - WM_TIMER messages can be delayed for a considerable time. If the delay time exceeds the interval, then the messages are combined together, thus, their loss also occurs [1].
       In order to at least somehow measure for a comparative analysis of the delay functions, you need a tool that allows you to accurately measure the time intervals of execution of a certain section of code. GetTickCount will not work because of the above. But the author found out about the ability to rely on the processor clock frequency for a certain time interval. Starting with Pentium III, processors usually contain a programmer’s real-time counter counter, Time Stamp Counter, TSC , which is a 64-bit register whose contents are incremented with each clock cycle [2]. Counting in the counter starts from zero every time at the start (or hardware reset) of the computer. You can get the counter value 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 insert puts the counter result 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 (read time stamp counter) command that allows you to immediately get the counter value with the RDTSC function [ 3 ] like this:    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 studied and before it:

    function RDTSC: Int64; register;
    asm
     rdtsc
    end;




    a:=tsc;
    Command;
    b:=tsc-a;


       In b will be the number of processor cycles elapsed during the execution of Command. But there is one point. The tsc call , which gives us the number of measures, must also spend some number of measures on this. And, for the fidelity of the result, it must be introduced as a correction subtracted from the obtained number of measures:

    a:=tsc;
    C:=tsc-a;
    a:=tsc;
    Command;
    b:=tsc-a-C;


       Everything would be fine, but it turns out experimentally that sometimes the values ​​of our correction C differ. The reason for this has been found. The point here is especially the functioning of the processor, or rather its conveyor. The advancement of machine instructions along 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 an instruction is, at best, determined by the throughput of the pipeline. The time interval that can be guaranteed to be guaranteed, receiving processor cycles - from 50 cycles [2]. It turns out that in the case of determining the correction, 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   end;
     calibrate_runtime:=crtm;
    end;


       Now that we have the necessary tool, we’ll experiment with the delay functions. Let's start with the well-known and widely used sleep :

    procedure Sleep(milliseconds: Cardinal); stdcall;

       In order to check the accuracy of the delay, we include the following code in our console program, in addition to the tsc code and calibrate_runtime code:    This code will be called from the program by setting several pau_dur values ​​(pauses) several times. Note that the number of measures during the pause is then divided by the value of the pause. So we find out the accuracy of the delay depending on its time. For the convenience of conducting the test and displaying / saving the test result, the following code was used ( see example 3 ):    In it, we execute cycleperms

    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;




    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.


    five times for each time interval (from 1 to 10,000 milliseconds), and also consider the average value. The table turns out. So, the obtained number of processor cycles in the course of such a study:
    TCC_DEF

       We are not observing the best picture. Since the processor frequency is approximately 1778.8 MHz ( see example 4 ), the clock values ​​for 1 millisecond should aim at an approximate number of 1778800. The accuracy of the sleep function does not give us this for 1, 10, 100 or 1000 milliseconds. Only in a ten-second period of time are the values ​​close. Perhaps, if in test 4 there were no 1781146, then the average value would be acceptable.
       What can be done? Leave the function and consider something else? Do not rush yet. I learned that you can manually set the reference time interval error using the timeBeginPeriod function [2]:    To maintain this high-precision resolution, additional system resources are used, so you need to call timeEndPeriod to release them when all operations are completed. The code of the cycleperms function for examining such a sleep ( see example 5 ):    There is still an inexplicable feature, timeBeginPeriod (1) , setting the resolution to 1 millisecond, does not start to produce an effect immediately, but only after calling sleep , therefore, in the code, after

    MMRESULT timeBeginPeriod(
        UINT uPeriod
       );




    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 inserted sleep (10) . Results of this study:
    Tcc

       Observed data are much better. The average over 10 seconds is pretty accurate. The average for 1 millisecond differs from it by only 1.7%. Accordingly, the differences for 10 ms are 0.056%, for 100 ms - 0.33% (strange happened), for 1000 ms - 0.01%. A shorter interval than 1 ms cannot be used in sleep . But it can be firmly said that sleep is suitable for 1 ms pauses provided that timeBeginPeriod (1) is executed , and the accuracy of sleep only increases with an increase in the specified time period ( see Example 6 ).

       The sleep function is based on the Native API function.NtDelayExecution , which has the following form [ 5 ]:    Let's try to test its delays, like sleep , but it will even take microseconds into account:    This function is not registered in windows.pas or any other file, so we will call it by adding the line:    Code in which we call the function and build the table of results, it should be corrected like this ( see example 7 ):    After examining the delays created by NtDelayExecution, we got interesting results:    It can be seen that it is useless to use this accuracy at intervals of less than 1 millisecond. Other delay intervals are slightly better than sleep

    NtDelayExecution(
      IN BOOLEAN              Alertable,
      IN PLARGE_INTEGER       DelayInterval );




    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;




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



    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.



    TCC_NTAPI

    without a changed resolution, but worse than with a high resolution sleep (this is understandable in principle, because here we did not create threads with a higher priority, and did not do anything to increase accuracy, similar to how timeBeginPeriod does it ). And if you add timeBeginPeriod ? Let's see what happens:
    NTAPI2

       At microsecond intervals, the situation is still the same. But at intervals starting from 1 millisecond, the difference relative to the 10-second value is 0.84%, which is better than the similar use of sleep (1.7%) -   NtDelayExecution gives a delay more accurately.
       When searching for programming tools for delays in code execution, another option was found [ 4], which seems to provide the ability to specify the interval in microseconds. This is WaitableTimer . You can work with it through the functions CreateWaitableTimer, SetWaitableTimer, WaitForSingleObjectEx . The type of the cyclepermks procedure where we added WaitableTimer : The    peculiarity of using WaitableTimer also requires us to modify the calculation of the correction obtained in calibrate_runtime :    After all, SetWaitableTimer and CloseHandle are also executed for the period of the number of processor clocks that we take into account. Immediately add the timeBeginPeriod call to the cyclepermks code

    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;




    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   end;
     calibrate_runtime2:=crtm;
    end;


    , hoping for the help of this procedure in increasing accuracy ( see example 8 ). Table of results:
    TCC_WFSO

       Alas, here we did not get the opportunity to set delays for intervals less than millisecond. The difference between 1 millisecond and 10 seconds is 5%. Compared to previous methods, this is worse.
       Before drawing conclusions, I will say a little about the actual measurement of time itself. In the above studies, the basis of the comparisons was the number of processor cycles, and each computer has a different one. If you need to convert it to units of time based on seconds, you need to do the following: using the NtDelayExecution 10-second delay , 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 safely convert smaller values ​​of the number of processor cycles to time values. In addition, it is recommended to set the application real-time priority.

       Conclusion As a result of the work carried out, it was found that it is possible to measure the time on a computer very accurately (even up to the length of time estimated at 50 processor cycles). This problem has been solved successfully. As for the ability to independently set the exact delays in the executable code, the situation is as follows: the best method detected allows us to do this with a resolution of not more than 1 millisecond, with a resolution error of about 0.84% ​​on the 1 ms interval. This is an NtDelayExecution function with setting permission by the timeBeginInterval procedure. The drawback of the function, compared to the less accurate sleep, is a cumbersome call and the presence of an insufficiently documented Native API. Use of the Native API is not recommended due to the possible incompatibility of individual APIs in different operating systems of the Windows family. In general, the obvious advantage of the NtDelayExecution function still forces one to make a choice in its favor.

    Examples:
     1. Determining the resolution of the system timer
     2. Output RDTSC
     3. Set the interval through sleep
     4. Find out the processor frequency
     5. Set the interval through sleep more accurately
     6. We examine the accuracy of setting the interval through sleep at different values
     7. Interval using NtDelayExecution
     8. Interval using WaitableTimer
     9. Find out the duration of one processor clock.
    Examples contain source code * .dpr files (in Delphi), a compiled console * .exe application, and (some) * .xls table of the results already obtained by the author (in a format supported by MS Excel). All examples are in one file .

    Literature:
     1. Russinovich M., Solomon D. Internal device Microsoft Windows. - St. Petersburg: Peter, 2005 .-- 992 p.
     2. Schupak Yu.A. Win32 API. Effective application development. - St. Petersburg: Peter, 2007 .-- 572 p.
     3. RDTSC - Wikipedia [ 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 ] The

       article was written on November 13, 2009 by begin_end . The author discussed some points considered in the article with slesh , who is grateful for such help.

    Also popular now: