High resolution replacement for Sleep
#1
SleepX()

SleepX() is a high resolution replacement for Sleep.

The principle used has an analogy with HiFi were a class B amplifier is used to get us within a neighbourhood of a desired voltage and then uses a class A amplifier to fine tune. The cost of this approach is much less than using only a class A amplifier, which are expensive.

The class B amplifier analogy uses Sleep and the class A amplifier analogy uses the Performance Counter, which has a resolution of 100ns with Windows 10 and later.

The following code has the SleepX() code and a usage example.

This is a typical output.
Code:
Quarter of a millisecond: .2503 ms

2 seconds: 2.0000003 s

Silly: 1234.0003 ms

1  1.0001 ms
2  2.0001 ms
3  3.0002 ms
4  4.0001 ms
5  5.0002 ms
6  6.0002 ms
7  7.0002 ms
8  8.0001 ms
9  9.0001 ms
10  10 ms
11  11.0002 ms
12  12.0002 ms
13  13.0001 ms
14  14.0001 ms
15  15.0001 ms
16  16.0002 ms
17  17.0002 ms
18  18.0002 ms
19  19.0002 ms
20  20.0001 ms
21  21.0003 ms
22  22.0002 ms
23  23.0001 ms
24  24.0001 ms
25  25 ms
26  26.0002 ms
27  27.0002 ms
28  28.0002 ms
29  29.0002 ms
30  30.0002 ms

The first example looks at a delay of a quarter of a millisecond. I doubt that anyone will have a use for that. The second example looks at a delay of two seconds. The third example looks at a 'silly' delay of 1234ms, The following looks at delays from 1ms to 30ms in steps of 1ms.

All the results have a sub micro accuracy inline with the Performance Counter on Windows 10 and later.

SleepX() has two parts: The first part is used for delays <= to 3ms and only polls the Performance Counter. This is expensive but is a short-lived expense and should not impact on the system performance. The second part uses the construct 'Sleep ( n-3 )' to get us within a neighbourhood of the target delay, and then we poll the Performance Counter to fine tune. 'Sleep ( n-3 )' needs a resolution of 1ms and why we use SetHiRes.

'Sleep ( n-3 )' does not use any CPU load. The CPU load only kicks in when we enter the 'class A' mode, so is an absolute value and should not impact on the system performance.

Why was 'n <= 3'.  It is reasonable to expect a 1ms resolution to give a delay of between n ms and n+1 ms. In practice, we can exceed n+1 ms and about one third of delays do just that. It is very rare, but values approaching n+1.5 ms have been seen. To mitigate that issue, 'n <= 3' was chosen.

That is it: A very simple idea to give a high resolution replacement for Sleep with a negligible CPU load.

It is worth noting that
Code:
SetHiRes
<Code to time>
RevokeHiRes
is acceptable, but it should not be used within a loop; for example a graphics application with 60, or so, fps. That will upset the system clock. It is better to use SetHiRes at the beginning of an application and RevokeHiRes when the higher resolution is no longer needed. If you forget to use RevokeHiRes SetHiRes will be cancelled when the process terminates. Microsoft neglects to mention that.

Code:
#COMPILE EXE
#DIM ALL
#INCLUDE "win32api.inc"

' SOURCE CODE:

Macro QPC = QueryPerformanceCounter qTimeNow

Macro SetHiRes
  MacroTemp Time
  Dim Time As TIMECAPS
  TimeGetDevCaps( Time, SizeOf(Time) )
  TimeBeginPeriod(Time.wPeriodMin)
  'Sleep 16 ' Pre Windows 10 - high resolution does not 'byte' until next clock tick.
End Macro

Macro RevokeHiRes
  MacroTemp Time
  Dim Time As TIMECAPS
  TimeGetDevCaps( Time, SizeOf(Time) )
  TimeEndPeriod(Time.wPeriodMin)
End Macro

Global qFreq As Quad

Sub SleepX( ByVal n As Double )
  Local qTarget, qTimeNow As Quad
  QPC
  qTarget = qTimeNow + n*qFreq*0.001
  If n <= 3 then
    Do : QPC : Loop Until qTimeNow >= qTarget ' Class A amplifier analogy
  Else
    ' Class B amplifier analogy followed by Classs A amplifier analogy
    Sleep ( n-3 ) : Do : QPC : Loop Until qTimeNow >= qTarget
  End If
End Sub

' ====================

' EXAMPLE USAGE:

Function Pbmain () As Long
Local i as Long
Local qStart, qStop As Quad

  QueryPerformanceFrequency qFreq

  SetHiRes

  QueryPerformanceCounter qStart
    SleepX(0.25)
  QueryPerformanceCounter qStop
  Print "Quarter of a millisecond:";(qStop - qStart)*1000/qFreq;"ms"
  Print

  QueryPerformanceCounter qStart
    SleepX(2000)
  QueryPerformanceCounter qStop
  Print "2 seconds:";(qStop - qStart)/qFreq;"s"
  Print

  QueryPerformanceCounter qStart
    SleepX(1234)
  QueryPerformanceCounter qStop
  Print "Silly:";(qStop - qStart)*1000/qFreq;"ms"
  Print

  For i = 1 to 30
    QueryPerformanceCounter qStart
      SleepX(i)
    QueryPerformanceCounter qStop
    Print i;" ";(qStop - qStart)*1000/qFreq;"ms"
  Next

  RevokeHiRes

  WaitKey$

End Function
Reply
#2
Comments here http://pump.richheimer.de/showthread.php?tid=74

SleepX() was ported from FreeBASIC. The class A amplifier analogy uses Timer because FreeBASIC's Timer is linked to the Performance Counter. PowerBASIC's Timer is linked to the System Clock with a resolution of 15.625 milliseconds so that is of no use for a high resolution Sleep. Even using SetHiRes it is too grainy and why the Performance Counter is used.

A FreeBASIC member's Performance Counter is 2.532753 MHz, so his accuracy was less than mine with Windows 10 at 10MHz.

I suggested enabling HPET. He is now getting 14.31818MHz. However, the overhead of getting the Performance Counter with HPET enabled is expensive compared with not enabling HPET. Nonetheless, his accuracy is better with HPET enabled. The consensus of opinion is not to have HPET enabled if you play games but that depends upon how old your machine is.

On my machine, enabling HPET reduces the accuracy so I stayed with FreeBASIC's Timer.

There is a higher resolution counter: the Time Stamp Counter. We could use RDTSC, but an easier way is to use PowerBASIC's TIX which FreeBASIC does not have.

With SleepX() a common result is of the form x.0001/x.0002. With SleepXX, using TIX, a common result is of the form x.0000x; a factor of 10 better than SleepX.

The SleepXX code and EXAMPLE USAGE does not use the Performance Counter at all. We do, however, need to know the CPU base frequency. My machine has a base frequency of 3.5GHz and 3.9GHz with Turbo, so I use 3.5GHz with 'MACRO CPUBaseFreq =  3.5*10^9'.

There is an overhead using TIX but it is negligible on my machine so I ignore it. That is also true using the Performance Counter.

The following code has the SleepXX() code and a usage example.

This is a typical output.
Code:
Quarter of a millisecond: 0.250063 ms

2 seconds:2000.000295 ms

Silly:1234.000299 ms

1  1.000118 ms
2  2.000035 ms
3  3.000062 ms
4  4.000162 ms
5  5.000051 ms
6  6.000068 ms
7  7.000059 ms
8  8.000058 ms
9  9.000058 ms
10  10.000071 ms
11  11.000063 ms
12  12.000072 ms
13  13.000066 ms
14  14.00007 ms
15  15.000066 ms
16  16.000069 ms
17  17.000071 ms
18  18.000076 ms
19  19.000068 ms
20  20.000073 ms
21  21.000074 ms
22  22.000077 ms
23  23.000079 ms
24  24.000072 ms
25  25.000102 ms
26  26.000071 ms
27  27.000081 ms
28  28.000147 ms
29  29.00008 ms
30  30.000085 ms
SleepXX
Code:
#COMPILE EXE
#DIM ALL
#break on
#INCLUDE "win32api.inc"

' SOURCE CODE:

MACRO CPUBaseFreq =  3.5*10^9

Macro SetHiRes
  MacroTemp Time
  Dim Time As TIMECAPS
  TimeGetDevCaps( Time, SizeOf(Time) )
  TimeBeginPeriod(Time.wPeriodMin)
  'Sleep 16 ' Pre Windows 10
End Macro

Macro RevokeHiRes
  MacroTemp Time
  Dim Time As TIMECAPS
  TimeGetDevCaps( Time, SizeOf(Time) )
  TimeEndPeriod(Time.wPeriodMin)
End Macro

Sub SleepXX( ByVal n As Double )
  Local qTarget, qTimeNow As Quad
  Tix qTimeNow
  qTarget = qTimeNow + n*CPUBaseFreq*0.001
  If n <= 3 then
    Do : Tix qTimeNow : Loop Until qTimeNow >= qTarget ' Class A amplifier analogy
  Else
    ' Class B amplifier analogy followed by Classs A amplifier analogy
    Sleep ( n-3 ) : Do : Tix qTimeNow  : Loop Until qTimeNow >= qTarget
  End If
End Sub

' ====================

' EXAMPLE USAGE:

FUNCTION PBMAIN () AS LONG
local i as Long
lOCAL qTime As Quad

  SetHiRes

  Tix qTime
    SleepXX(0.25)
  Tix End qTime
  Print "Quarter of a millisecond:";format$(qTime/CPUBaseFreq/10^15," ####.######");" ms"
  Print

  Tix qTime
    SleepXX(2000)
  Tix End qTime
  Print "2 seconds:";Format$(qTime/CPUBaseFreq/10^15, "####.######");" ms"
  Print

  Tix qTime
    SleepXX(1234)
  Tix End qTime
  Print "Silly:";ForMat$(qTime/CPUBaseFreq/10^15, "####.######");" ms"
  Print

  For i = 1 to 30
    Tix qTime
      SleepXX(i)
    Tix End qTime
    Print i;" ";Format$(qTime/CPUBaseFreq/10^15, "####.######");" ms"
  Next

  RevokeHiRes

  WaitKey$

END FUNCTION
Reply
#3
Revised SleepXX

This version includes code written by Stuart McLachlan to determine CPUBaseFreq programmatically.

So we now have our machine doing the 'dirty work'.
Code:
#COMPILE EXE
#DIM ALL
#break on
#INCLUDE "win32api.inc"

' SOURCE CODE:

Macro SetHiRes
  MacroTemp Time
  Dim Time As TIMECAPS
  TimeGetDevCaps( Time, SizeOf(Time) )
  TimeBeginPeriod(Time.wPeriodMin)
  'Sleep 16 ' Pre Windows 10
End Macro

Macro RevokeHiRes
  MacroTemp Time
  Dim Time As TIMECAPS
  TimeGetDevCaps( Time, SizeOf(Time) )
  TimeEndPeriod(Time.wPeriodMin)
End Macro

GLOBAL CPUBaseFreq AS QUAD

FUNCTION GetBaseCPUFreq() AS QUAD ' By Stuart McLachlan
  LOCAL retval , hkey AS DWORD
  LOCAL MHz AS DWORD
  retval = RegOpenKeyEx( %HKEY_LOCAL_MACHINE,"HARDWARE\DESCRIPTION\System\CentralProcessor\0"  ,0,%key_query_value,hkey)
  retval = regqueryvalueex(hkey,"~Mhz",0,%REG_DWORD,MHZ,4)
  retval = regclosekey(hkey)
  FUNCTION = MHZ * 10 ^6
END FUNCTION

Sub SleepXX( ByVal n As Double )
  Local qTarget, qTimeNow As Quad
  Tix qTimeNow
  qTarget = qTimeNow + n*CPUBaseFreq*0.001
  If n <= 3 then
    Do : Tix qTimeNow : Loop Until qTimeNow >= qTarget ' Class A amplifier analogy
  Else
    ' Class B amplifier analogy followed by Classs A amplifier analogy
    Sleep ( n-3 ) : Do : Tix qTimeNow  : Loop Until qTimeNow >= qTarget
  End If
End Sub

' ====================

' EXAMPLE USAGE:

FUNCTION PBMAIN () AS LONG
Local i as Long
Local qTime As Quad

  CPUBaseFreq = GetBaseCPUFreq
 
  SetHiRes

  Tix qTime
    SleepXX(0.25)
  Tix End qTime
  Print "Quarter of a millisecond:";format$(qTime/CPUBaseFreq*10^3," ####.######");" ms"
  Print

  Tix qTime
    SleepXX(2000)
  Tix End qTime
  Print "2 seconds:";Format$(qTime/CPUBaseFreq*10^3, "####.######");" ms"
  Print

  Tix qTime
    SleepXX(1234)
  Tix End qTime
  Print "Silly:";ForMat$(qTime/CPUBaseFreq*10^3, "####.######");" ms"
  Print

  For i = 1 to 30
    Tix qTime
      SleepXX(i)
    Tix End qTime
    Print i;" ";Format$(qTime/CPUBaseFreq*10^3, "####.######");" ms"
  Next 

  RevokeHiRes

  WaitKey$

END FUNCTION
Reply


Forum Jump:


Users browsing this thread: 1 Guest(s)