MacroTimersQPC.inc

Post your FreeBASIC source, examples, tips and tricks here. Please don’t post code without including an explanation.
St_W
Posts: 1626
Joined: Feb 11, 2009 14:24
Location: Austria
Contact:

Re: MacroTimersQPC.inc

Post by St_W »

MrSwiss wrote:[...] for sure, using some strange data types here: Large_Integer (looks like a custom Type), ULongLong (looks like C/C++) etc.
These are Windows Datatypes; see: https://msdn.microsoft.com/en-us/librar ... 83751.aspx

There's nothing wrong with using them in a Windows program using the Win32-API. Actually it's probably even preferable to comply with the documentation and prevent type errors, e.g. the actual type could change on other systems (if there's e.g. something like Win32 on non-x86 architecture).
MrSwiss wrote:I still don't like the .inc stuff on FB, simply because the IDE I'm using (FBEdit) is either not
*highlighting* correctly, or disallows me, to open an .inc from within (after recognizing it).
While .bi is the default for FreeBasic there's nothing wrong with .inc for include; basically a matter of personal preference, which shouldn't be dictated to others having a different opinion. (I'm using FBedit too and also ran into the highlighting issue, though)

@deltarho[1859]: In the code there are "$" suffixes. Are those intentional? The compiler doesn't complain about the syntax, but I guess these should be removed as FB doesn't have type suffixes.

Code: Select all

SetDecimalPlaces$(k)
Thank you for sharing this. I really like that PowerBasic programmers are now considering FreeBasic and do provide very useful, professional quality contributions.
deltarho[1859]
Posts: 4313
Joined: Jan 02, 2017 0:34
Location: UK
Contact:

Re: MacroTimersQPC.inc

Post by deltarho[1859] »

@St_W
In the code there are "$" suffixes. Are those intentional?


There are fives instances of SetDecimalPlaces and two of them end with '$'. I don't use suffixes unless I am compelled to. I use my own type of Hungarian Notation - prefixing with an 's' for string for example. I've just checked my own source code in WinFBE and there are the same two offenders. I wondered whether the autocomplete had thrown a 'wobbly' but it doesn't appear so.

I find this so spooky I will need to have a lie down in a darkened room.
I really like that PowerBasic programmers are now considering FreeBasic
My first post was only a couple of weeks ago and I am slowly reading the Help index one item at a time; although coding is getting in the way. <smile> Every now and again I think "Wow, it does what?". Some of the commands are unbelievably powerful.
Thank you for sharing this.
My pleasure and thanks for thanking me.
adele
Posts: 47
Joined: Jun 13, 2015 19:33

Re: MacroTimersQPC.inc

Post by adele »

[commit][/commit]@deltarho[1859]
Excellent code. But even better is the documentation. Thank you.

But, to stay in "Tips & Tricks":

@MrSwiss
You wrote: "[] don't like the .inc [] because [] (FBEdit) is either not *highlighting* []..."

Trick & Try: enter ".bas.bi.inc." in (FBedit menu) -> optionen->code editor, at "3 o´clock" (== vertical middle / right half )


@deltarho[1859]
Readability: Just cosmetics. I needed it, because in my Virtual Machine I had a deviation of about 0.7 percent just timing the "sleep 500", and up to 17.0 (!!!) percent in your ' TimerUsage.BAS ' . I didn´t want to believe this.
On the real machine, this normalized. But I needed to know what the numbers want to tell me...

Code: Select all

' Cosmetics :)
Print sPerformanceCounterFrequency

StartTimer(0)
  Sleep (500,1)
StopTimer(0)
Print "Slept 500 ms..."
Print sTimeTaken(0, 3, False)
Print

StartTImer(0)
  Sleep (100,1)
Print "Slept 100 ms..."  
StopTimer(0)
Print sTimeTaken(0, -1, False) ' No formatting
Print

' Cosmetics_ENDs :)
Usability: I trust in your code and agree with you that the granularity is by far enough to perform serious profiling/tuning of algorithms/functions.


st_w wrote: "Thank you [..] (who) provide very useful, professional quality contributions."

I repeat:
very useful, professional quality contribution
(..cannot find better words...)

Have a nice day. And thank you once again.

Adele
MrSwiss
Posts: 3910
Joined: Jun 02, 2013 9:27
Location: Switzerland

Re: MacroTimersQPC.inc

Post by MrSwiss »

@adele,

thanks for your Tip. I've already done that, but it only solves the highlighting issue.
The second one, I'm referring to "opening the file from within (the IDE)", this is a option, I've
discovered "by chance": if you have a #Include "DateTime.bi" (for arguments sake only) and
highlight it (without the double quotes, but including the extension), then click the file open
Icon, the .bi is instantly opened in the Editor. This is "not working" except with .bi/.bas.
Probably "hard coded" somewhere ... similar to the "file open" dialog.

@St_W,

don't know where you're getting those funny ideas of yours, from:
St_W wrote:... basically a matter of personal preference, which shouldn't be dictated to others having a different opinion. (I'm using FBedit too and also ran into the highlighting issue, though)
This is exactly what I did: stating my own personal opinion. You seem to be, the dictating one, here.
deltarho[1859]
Posts: 4313
Joined: Jan 02, 2017 0:34
Location: UK
Contact:

Re: MacroTimersQPC.inc

Post by deltarho[1859] »

Here is another macro to add to the above although it doesn't time an interval but creates one.

A simple analogy of the method employed is to consider amplification in a Hi-Fi system. We could have a powerful Class B amplifier which doesn't sound too good but didn't cost much. On the other hand, we could have a powerful Class A amplifier which sounds great but both our bank manager and partner would like a word with us at our earliest opportunity.

A hybrid, however, would use the Class B amplifier to get us within a neighbourhood of our desired sound and then use a very much less powerful Class A amplifier to fine tune resulting in a sound close to the more powerful Class A amplifier at a very much smaller combined cost.

With a millisecond timer linked to Sleep if we think only of resolution, neglecting access time, then a request for a five millisecond Sleep would see a delay of, roughly, five to six milliseconds. In general, a request for n milliseconds would see a delay of, roughly, n to n+1 milliseconds. A request for n-1 milliseconds would see a delay of, roughly, n-1 to n milliseconds.

The first part of the method does just that: It requests a Sleep of n-1 milliseconds. This is our Class B amplifier.

We then enter a loop polling QueryPerformanceCounter until we reach n milliseconds. This is our Class A amplifier.

In practice the time in the loop will vary between, roughly, no time at all or one millisecond for any value of n and we will never know at what point the cross-over occurred but that doesn't concern us - all that we are interested in is hitting n milliseconds in good time. Whilst in the loop we will have CPU activity but since we are in there for only one millisecond at most then I cannot see this being an issue.

If we do multiple tests it is bad practice to have them done 'back to back' - it is better to insert a 'breathing space' between tests and, preferably a random one.

Here is the macro.

Code: Select all

#Macro VeryHiResSleep( n ) ' n >= 1
  Scope
    Dim As ULongInt Target, Now
    QueryPerformanceCounter Cast( Large_Integer Ptr, @Now )
    Target = Now + n*liFreq/1000
    Sleep ( n-1, 1 ) ' Class B amplifier
    Do
      QueryPerformanceCounter Cast( Large_Integer Ptr, @Now ) ' Class A amplifier
    Loop Until Now >= Target
  End Scope
#EndMacro

We need Sleep linked to a one millisecond timer and this can be done easily via the StartHiResClock macro in the opening post. This should be executed well before employing VeryHiResSleep() as it incurs a delay itself before the change in resolution occurs. The best place would be at the beginning of a console application or in WM_InitDialog.

Here is the test code using the macro timers in the opening post.

Code: Select all

StartHiResClock
'...
'...
'...
Dim as Long n = 10
For i = 1 To 20
  StartTimer(0)
    VeryHiResSleep( n )
  StopTimer(0)
  Print sTimetaken(0,3,0);
  If i mod 5 = 0 THEN Print
  Sleep( Rnd*100+50,1) ' Have a random breather
Next

This is what I get:

Code: Select all

10.003ms 10.008ms 10.036ms 10.008ms 10.002ms
10.002ms 10.003ms 10.073ms 10.052ms 10.003ms
10.021ms 10.005ms 10.003ms 10.003ms 10.002ms
10.003ms 10.002ms 10.002ms 10.04ms  10.004ms
Requesting 10 milliseconds from just Sleep linked to a millisecond timer would see a worst case overshoot of up to, roughly, one millisecond or, to put that another way, 1000 microseconds. The worst case overshoot in the above table is 73 microseconds.

So, it would seem that VeryHiResSleep() is putting us in a very different place.

The worst case overshoot, for some reason, reduces for higher delay requests and are quite remarkable for a delay of 100 milliseconds although it is debatable whether we should be using VeryHiResSleep() for such a 'large' delay.

We should expect a delay of two milliseconds, for example, to give very volatile results but this is not the case. Here is a typical two millisecond delay output.

Code: Select all

2.002ms 2.023ms 2.008ms 2.003ms 2.008ms
2.002ms 2.003ms 2.002ms 2.003ms 2.007ms
2.024ms 2.002ms 2.002ms 2.002ms 2.003ms
2.002ms 2.003ms 2.015ms 2.005ms 2.002ms

It is worth noting that Sleep(0,1) behaves as Microsoft's Sleep at MSDN so with a one millisecond request of VeryHiResSleep() we find ourselves in the performance counter loop pretty quickly. In this case we simply get a millisecond burst of CPU activity.

For me this post is of only academic interest as I have no use for it, <smile>, a standard Sleep linked to a millisecond timer does me just fine. Whilst mentioning that I still find many folk at different forums requesting 5 or 10 milliseconds from their compiler's Sleep ticking over at the Windows default of 64Hz ie a period of 15.625ms. That regime has no idea what 5 or 10 milliseconds is and they could end up with 15 or 16 milliseconds.

Have fun.
deltarho[1859]
Posts: 4313
Joined: Jan 02, 2017 0:34
Location: UK
Contact:

Re: MacroTimersQPC.inc

Post by deltarho[1859] »

A request for n-1 milliseconds would see a delay of, roughly, n-1 to n milliseconds.
Occasionally, we overshoot n milliseconds so there is no need to poll QueryPerformanceCounter. We can test if an overshoot has occurred but this would incur a greater overshoot.

A solution is to use Sleep( n-2,1 ). Occasionally, we would overshoot n-1 but we would never overshoot n. This will limit us to requests of n >= 2 and a possible CPU burst of about two milliseconds.

Tests have shown a worst case overshoot of three microseconds.

If it is important to be this precise then go for Sleep( n-2, 1 ). If the occasional 'spike' of 70 microseconds or so is acceptable then stay with Sleep( n-1, 1 )
deltarho[1859]
Posts: 4313
Joined: Jan 02, 2017 0:34
Location: UK
Contact:

Re: MacroTimersQPC.inc

Post by deltarho[1859] »

I recently provided a link to MacroTimersQPC.inc and then used the latest version which had not been published and was designed differently. What I did can be described as an almighty cock-up.

Below is the latest version.

The macro VeryHiResSleep has been replaced by a more robust version. The function 'nTimeTaken( As Ubyte ) As Double' has been added. This is similar to 'sTimeTaken( As Long, As Long, As Long ) As String' but returns only the time taken in millisecods as Double.

Example:

Code: Select all

StartTimer(5)
' Code to time
StopTimer(5)
Print nTimeTaken(5)

We don't have to Print straight away, as we don't have to execute sTimeTaken straight away. When we do Print, as with sTimeTaken, the corresponding timer is released for use again.

There is no need to use StartHiResClock/StopHiResClock now. When we want to change the system timer resolution to one millisecond, for example, we can now simply add

Code: Select all

SetHiRes = timeBeginPeriod(1)
and revoke it with

Code: Select all

RevokeHiRes = timeEndPeriod(1)
We must revoke nowadays and not rely on the system to do it for us on a process closure.

MacroTimersQPC.inc is now called MultipleTimers.inc to distinguish it from the original MacroTimersQPC.inc.

MacroTimersQPC.inc was originally developed at PowerBASIC using the Performance Counter. PowerBASIC has a TIX command which uses the Time Stamp Counter and there we have MacroTimersTIX.inc.

We can use MultipleTimers.inc in the same way as MacroTimersQPC.inc - no change in your code is required. The change is internal and not external. An analogy is a Windows API changing internally but not externally.

So future code where you would have used MacroTimersQPC.inc use MultipleTimers.inc instead.

MultipleTimers.inc

Code: Select all

#Include Once "windows.bi"
#Include Once "string.bi"
#Include Once "win\mmsystem.bi"

Dim Shared As ULongInt liFreq
Dim Shared As ULongInt liStart(0 To 15), liStop(0 To 15)
Dim Shared As ULongInt liTotalTime( 0 To 15 )
Dim Shared As ULongInt liFastestTime( 0 To 15)
Dim Shared As ULongInt liSlowestTime( 0 To 15)
Dim Shared As ULongInt liTimerCallCount( 0 To 15 )
Dim Shared As String sFuncName( 0 To 15 )
Dim Shared As MMRESULT SetHiRes, RevokeHiRes

QueryPerformanceFrequency Cast( Large_Integer Ptr, @liFreq)

#Define sPerformanceCounterFrequency LTrim(Format(liFreq, "###,###,###,###")) 

#Macro StartHiResClock
  Scope
    Dim As TIMECAPS tc
    TimeGetDevCaps( @tc, SizeOf(tc) )
    TimeBeginPeriod(tc.wPeriodMin)
  End Scope
  Sleep (16,1) ' Tests have shown that the new resolution will not 'bite' until next 'old' tick
#EndMacro

#Macro StopHiResClock
  Scope
    Dim As TIMECAPS tc
    TimeGetDevCaps( @tc, SizeOf(tc) )
    TimeEndPeriod(tc.wPeriodMin)
  End Scope
  Sleep (2,1) ' Tests have shown that the new resolution will not 'bite' until next 'old' tick
#EndMacro

#Macro StartTimer(i)
  sFuncName(i) = __FUNCTION__
  QueryPerformanceCounter Cast( Large_Integer Ptr, @liStart(i) )
#EndMacro

#Define StopTimer(i) QueryPerformanceCounter Cast( Large_Integer Ptr, @liStop(i) )

#Macro StopTimer_UpdateTotalTime(i)
  QueryPerformanceCounter Cast( Large_Integer Ptr, @liStop(i) )
  liTotalTime(i) += ( liStop(i) - liStart(i) )
  liTimerCallCount(i) += 1
#EndMacro

#Macro StopTimer_UpdateFastestTime(i)
  QueryPerformanceCounter Cast( Large_Integer Ptr, @liStop(i) )
  If liTimerCallCount(i) = 0 Then
    liFastestTime(i) = liStop(i) - liStart(i)
  Else
    liFastestTime(i) = Min( liFastestTime(i), liStop(i) - liStart(i) )
  End If
  liTimerCallCount(i) += 1
#EndMacro

#Macro StopTimer_UpdateSlowestTime(i)
  QueryPerformanceCounter Cast( Large_Integer Ptr, @liStop(i) )
  liSlowestTime(i) = Max( liSlowestTime(i), liStop(i) - liStart(i) )
  liTimerCallCount(i) += 1
#EndMacro

#Macro StopTimer_UpdateFastSlowTime(i)
  Dim As ULongInt liDummy
  QueryPerformanceCounter Cast( Large_Integer Ptr, @liStop(i) )
  liDummy = liStop(i) - liStart(i)
  If liTimerCallCount(i) = 0 Then
    liFastestTime(i) = liDummy
    liSlowestTime(i) = liDummy
  Else
    liFastestTime(i) = Min( liFastestTime(i), liDummy )
    liSlowestTime(i) = Max( liSlowestTime(i), liDummy )
  End If
  liTimerCallCount(i) += 1
#EndMacro

#Macro VeryHiResSleep( n ) ' n >= 1
  Dim As ULongInt Target, TimeNow
  QueryPerformanceCounter Cast( Large_Integer Ptr, @TimeNow )
  Target = TimeNow + n*liFreq/1000
  If n < 3 then ' Performance counter will be used for less than two milliseconds
    Do
      QueryPerformanceCounter Cast( Large_Integer Ptr, @TimeNow ) ' Class A amplifier analogy
    Loop Until TimeNow >= Target
  Else
    SetHiRes = timeBeginPeriod(1)
    Sleep ( n-2, 1 ) ' Class B amplifier analogy
    RevokeHiRes = timeEndPeriod(1)
    Do
     QueryPerformanceCounter Cast( Large_Integer Ptr, @TimeNow ) ' Class A amplifier analogy
    Loop Until TimeNow >= Target
  End If
#EndMacro

#Macro SetDecimalPlaces( a )
  Select Case a+4*(a>3)+1
    Case 1
      s = "######"
    Case 2
      s = "######.#"
    Case 3
      s = "######.##"
    Case 4
      s = "######.###"
  End Select
#Endmacro

Declare Function nTimeTaken( As Ubyte ) As Double
Declare Function sTimeTaken( As Long, As Long, As Long ) As String
Declare Function sTotalTimeTaken( As Long, As Long, As Long ) As String
Declare Function sFastestTimeTaken( As Long, As Long, As Long ) As String
Declare Function sSlowestTimeTaken( As Long, As Long, As Long ) As String
Declare Function sFastSlowTimeTaken( As Long, As Long, As Long ) As String
Declare Function sAverageTimeTaken( As Long, As Long, As Long ) As String
Declare Function FormatOutput( As Long, As ULongInt, As String, As String, As ULongInt, As Long, flag As Long ) As String

' ~~~~~~~~~~

Private Function nTimeTaken(Byval i As Ubyte) As Double
  Dim n As Double
  n = (liStop(i) - liStart(i))*1000/liFreq
  liStart(i) = 0
  Return n
End Function

Private Function sTimeTaken( i As Long, j As Long, flag As Long) As String
Dim s As String
Dim k As Long

  If j>= 0 Then
    k = Min(j,7)
    SetDecimalPlaces( k )
    s = " " + Format( (liStop(i) - liStart(i)) * _
      IIf(k<4, 1000, 1000000)/liFreq, s) + IIf(k<4, "ms", "us")
  Else
    s = str( (liStop(i) - liStart(i)) * 1000/liFreq ) + "ms"
  EndIf
  If flag = True Then
    s = "[" + LTrim(Str(i)) + "] in " + sFuncName(i) + " Single:" + s
  EndIf
  liStart(i) = 0
  Return s
  
End Function

' ~~~~~~~~~~

Private Function sFastestTimeTaken( i As Long, j As Long, flag As Long ) As String
Dim s As String

  s = FormatOutPut( i, liFastestTime(i), " Fastest(", sFuncName(i), liTimerCallCount(i), j, flag )
  liStart(i) = 0
  liTimerCallCount(i) = 0
  liFastestTime(i) = 0
  Return s
  
End Function

' ~~~~~~~~~~
 
Private Function sSlowestTimeTaken( i As Long, j As Long, flag As Long ) As String
Dim s As String
  
  s = FormatOutput( i, liSlowestTime(i), " Slowest(", sFuncName(i), liTimerCallCount(i), j, flag )
  liStart(i) = 0
  liTimerCallCount(i) = 0
  liSlowestTime(i) = 0
  Return s
  
End Function

' ~~~~~~~~~~
 
Private Function sFastSlowTimeTaken( i As Long, j As Long, flag As Long ) As String
Dim s As String
Dim k As Long
  
  If j >= 0 Then
    k = Min(j,7)
    SetDecimalPlaces(k)
    s = " " + Format( liFastestTime(i) * IIf(k<4, 1000, 1000000)/liFreq, s ) + "~" + _
      Format( liSlowestTime(i)* IIf(k<4, 1000, 1000000)/liFreq, s) + IIf(k<4, "ms", "us")
  Else
    s = Str( liFastestTime(i)*1000/liFreq ) + "~" + LTrim(Str( liSlowestTime(i)*1000/liFreq )) + "ms"
  End If
  If flag = True Then
    s = "[" + LTrim(Str(i)) + "] in " + sFuncName(i) + " FastSlow(" + LTrim(Str(liTimerCallCount(i))) + "):" + s
  End If
  liStart(i) = 0
  liTimerCallCount(i) = 0
  liFastestTime(i) = 0
  liSlowestTime(i) = 0
  Return s
  
End Function

' ~~~~~~~~~~

Private Function sTotalTimeTaken( i As Long, j As Long, flag As Long ) As String
Dim s As String

  s = FormatOutput( i, liTotalTime(i), " Total(", sFuncName(i), liTimerCallCount(i), j, flag )
  liStart(i) = 0
  liTotalTime(i) = 0
  liTimerCallCount(i) = 0
  Return s
  
End Function

' ~~~~~~~~~~

Private Function sAverageTimeTaken( i As Long, j As Long, flag As Long ) As String
Dim s As String
Dim k As Long

  If j >= 0 Then
    k = Min(j,7)
    SetDecimalPlaces(k)
    s = " " + Format( liTotalTime(i) * IIf(k<4, 1000, 1000000)/(liFreq * liTimerCallCount(i)), s) + IIf(k<4, "ms", "us")
  Else
    s = Str(liTotalTime(i)*1000/(liFreq * liTimerCallCount(i))) + "ms"
  End If
  If flag = True Then
    s = "[" + LTrim(Str(i)) + "] in " + sFuncName(i) + " Average(" + LTrim(Str(liTimerCallCount(i))) + "):" + s
  End If
  liStart(i) = 0
  liTotalTime(i) = 0
  liTimerCallCount(i) = 0
  Return s
  
End Function

' ~~~~~~~~~~

Private Function FormatOutput( ourTimer As Long, Scheme as ULongInt, sScheme As String, _
  FuncName As String, Counter as ULongInt , j As Long, flag As Long ) As String
Dim k As Long
Dim s As String
 
  If j >= 0 Then
    k = Min( j, 7 )
    SetDecimalPlaces( k )
    s = " " + Format(Scheme * IIf(k<4, 1000, 1000000)/liFreq, s) + IIf(k<4, "ms", "us")
  Else
    s = Str(Scheme*1000/liFreq) + "ms"
  End If
  If flag = True Then
    s = "[" + LTrim(Str(ourTimer)) + "] in " + FuncName + sScheme + LTrim(Str(Counter)) + "):" + s
  End If
  Return s
 
End Function

' ~~~~~~~~~~
Post Reply