Page 1 of 1

High resolution timer module

Posted: Sat Sep 10, 2022 11:57 am
by pTb
Hi all!
Doing some experimenting with a timer module that I want to use in some of my programs.

As it is now, I use prototypes to be able to call the correct function depending on if the target machine has a high resolution timer available or not. The drawback with this approach is that if I forget the parenthesis after the function name, I will not get the time but the address to the chosen procedure.

Feel free to comment on my approach and explain to me if You would think it would be better to use an Interface instead? I'm not very found of Interfaces since it's a lot of extra code just to initialize it, but I might be wrong. :lol:

The code is built with a Module for handling the timers (and selecting timer).

There's only two functions to use the Module from the outside:

Code: Select all

Timer::Start() ; Keeps track of a start value for the timer (resets the timer)
Timer::GetNanoSeconds() ; Reads the timer
I wrote some test code to try this out:
Image

Sample output from the test code:

Code: Select all

Module Timer is choosing a high resolution timer
Testing HiRes timer with delay 500 ms
5010020000
 
Testing speed in a loop 1 with size 10000000
1156998000
2317878000
3582612000
4733897000
5888867000
 
Testing speed in a loop 2 with size 10000000
1172478000
1171137000
1162552000
1165073000
1167249000
The complete source:

Code: Select all


DeclareModule Timer
  
  Prototype protoStart()
  Prototype.q protoGetNanoSeconds()
  
  Define Start.protoStart
  Define GetNanoSeconds.protoGetNanoSeconds
  
EndDeclareModule

Module Timer
  
  EnableExplicit
  
  Define *StartTimer, *GetTimerNanoSeconds
  
  Declare.l hiResTimerExists()
  Declare useStandardTimer()
  Declare useHiResTimer()
  Declare StartHiresTimer()
  Declare StartStandardTimer()
  Declare.q GetHiresTimer()
  Declare.q GetStandardTimer()
  
  Global.q frequency, counterStartValue, counter
  
  #debugAlwaysFailHiResTimer = #False
  
  If hiResTimerExists() = #False
    useStandardTimer()
  Else
    useHiResTimer()
  EndIf
  
  Start = *StartTimer
  GetNanoSeconds = *GetTimerNanoSeconds
  
  Procedure.l hiResTimerExists()
    
    QueryPerformanceFrequency_(@frequency)
    CompilerIf #debugAlwaysFailHiResTimer
      frequency = 0
    CompilerEndIf
    
    If frequency > 0
      Debug "Module Timer is choosing a high resolution timer"
      ProcedureReturn #True
    Else
      Debug "Module Timer can't find the high resolution timer, fallback to standard 1ms resolution timer."
      ProcedureReturn #False
    EndIf
    
  EndProcedure
  
  Procedure useStandardTimer()
    
    Shared *StartTimer, *GetTimerNanoSeconds
    
    *StartTimer = @StartStandardTimer()
    *GetTimerNanoSeconds = @GetStandardTimer()
    
  EndProcedure
  
  Procedure useHiResTimer()
    
    Shared *StartTimer, *GetTimerNanoSeconds
    
    *StartTimer = @StartHiresTimer()
    *GetTimerNanoSeconds = @GetHiresTimer()
    
  EndProcedure
  
  Procedure StartHiresTimer()
    
    QueryPerformanceCounter_(@counter)
    counterStartValue = counter
    
  EndProcedure
  
  Procedure StartStandardTimer()
    
    counterStartValue = ElapsedMilliseconds()
    
  EndProcedure
  
  Procedure.q GetHiresTimer()
    
    QueryPerformanceCounter_(@counter)
    ProcedureReturn Abs(counter - counterStartValue) / frequency * 10e9
    
  EndProcedure
  
  Procedure.q GetStandardTimer()
    
    ProcedureReturn (ElapsedMilliseconds() - counterStartValue) * 10e6
    
  EndProcedure
  
EndModule

#testDelay = 500
#loopSize = 1e7

Declare LetTheGoodLoopRoll()

; ***** Test 1

Timer::Start()
Debug "Testing HiRes timer with delay " + Str(#testDelay) + " ms"
Delay(#testDelay)
Debug Timer::GetNanoSeconds()

; ***** Test 2 - Accumulated timing

Debug " "
Debug "Testing speed in a loop 1 with size " + Str(#loopSize)

Timer::Start()

For j = 1 To 5
  LetTheGoodLoopRoll()
Next

; ***** Test 3 - Lap times

Debug " "
Debug "Testing speed in a loop 2 with size " + Str(#loopSize)
For j = 1 To 5
  LetTheGoodLoopRoll()
Next

Procedure LetTheGoodLoopRoll()
  
  Timer::Start()
  For i = 1 To #loopSize
    g = Random(1e6, 1)
  Next
  Debug Timer::GetNanoSeconds()
  
EndProcedure



// Moved from "Tricks 'n' Tips" to "Coding Questions" (Kiffi)

Re: High resolution timer module

Posted: Sat Sep 10, 2022 12:01 pm
by pTb
Comments to the timer resolution.
It depends on each and every machine. My current machine has a resolution of 10 million. My old machine a bit under that... and so on... The module should calculate the values to nanoSeconds (a billionth of a second) but that doesn't mean the hardware has that resolution...

Re: High resolution timer module

Posted: Sat Sep 10, 2022 1:45 pm
by Little John
pTb wrote: Sat Sep 10, 2022 11:57 am The drawback with this approach is that if I forget the parenthesis after the function name, I will not get the time but the address to the chosen procedure.
To be more precise: According to my tests with PB 6.00 LTS on Windows, this problem does exist with GetNanoSeconds(), but not with Start().

I'd write two "wrapper" functions and declare them in the public module part like so:

Code: Select all

DeclareModule Timer
   Declare   Start()
   Declare.q GetNanoSeconds()
EndDeclareModule
And handle all the prototype stuff in the private part of the module.

Re: High resolution timer module

Posted: Sat Sep 10, 2022 4:45 pm
by Axolotl
no offense.
I personally prefer to make a task as simple and short as possible. (And always to do without modules.)
Here is my all in one procedure.

Code: Select all

;-■--- High Quality Performance Counter -------------------------------------------------------------------------------

  Procedure.i ElapsedMicroseconds()  ; returns the current value in microsecondes or ZERO! 
    ;
    ; High Quality Performance Counter 
    ;   retrieves the current value of the high-resolution performance counter.
    ;   with Accuracy = 0.00083 ms (!!!) 
    ;
    Static frequency.q   ; current performance-counter frequency, in counts per second 
    Protected count.q    ; 

    If frequency = 0 
      If QueryPerformanceFrequency_(@frequency)  ;:: if the installed hardware supports a high-resolution performance counter, the return value is nonzero. 
        Debug "High-Resolution Performance Counter -> Frequency = " + frequency + " counts / second.", 9 
        ;:: frequency / 10000    ; .. divide frequency by 10000 for tenths of a millisecond 
        ; frequency / 1000   ;:: in milli seconds 
        ; frequency / 1000   ;:: in micro seconds 
        frequency / 1000000      ;:: in micro seconds 
        Debug "  -> Frequency = " + frequency + " counts / second.", 9 
     ;Else 
     ;  GetLastError_() 
      EndIf 
    EndIf ; frequency = 0 

    If QueryPerformanceCounter_(@count)   ;:: if the function succeeds, the return value is nonzero.
      Debug "  -> Count = " + count + " is Count / Frequency = " + Str(count / frequency), 9 
      ProcedureReturn count / frequency 
   ;Else 
   ;  GetLastError_() 
    EndIf 
    ProcedureReturn 0 ;:: the function fails :) 
  EndProcedure 
  ; 
  ; ;:: Usage: 
  ; Define t = ElapsedMicroseconds()
  ; Delay(50) 
  ; Debug "Diff = " + Str(ElapsedMicroseconds() - t) + " " 

Re: High resolution timer module

Posted: Sat Sep 10, 2022 7:47 pm
by pTb
Little John wrote: Sat Sep 10, 2022 1:45 pm
I'd write two "wrapper" functions and declare them in the public module part like so
That will actually be a good idea. Thx! Not used to use procedures as pointers, so this module was more or less a little education for me.

Re: High resolution timer module

Posted: Sat Sep 10, 2022 7:57 pm
by pTb
Axolotl wrote: Sat Sep 10, 2022 4:45 pm no offense.
I personally prefer to make a task as simple and short as possible. (And always to do without modules.)
Non taken. You were told to feel free to comment. :wink:

I was actually quite reluctant to Modules until a couple of years ago. But now when I know how they behave, I write no program over 1k lines that doesn't use both modules and projects.

A bit over 10 years ago I wrote a program with a 130 k source code that doesn't use any module. I'm scared down to my living bones to do any alternations in that code! :D

What I love with modules is that I can isolate code and not have to remember each and every name, so I don't overwrite anything important. Resources are still shared though, which I'm not always happy with...

So, I would say that modules make my programs much more simple, understandable and maintainable, now that I'm used to write in modules.

Still it's a free choice. That's the beauty of PB, that it allows us to use many different approaches. 8)

Re: High resolution timer module

Posted: Sun Sep 11, 2022 1:24 pm
by pTb
Updated source as if anyone would be interested in this simple code :P
(I made a last minute update before uploading the first code, which made it not to behave the same as the output I posted on the top).
Now it should behave the same and I took Little Johns advice to make a "wrapper" instead. Much better. thx again!

Code: Select all

DeclareModule Timer
  
  Declare Start()
  Declare.q GetNanoSeconds()
  
EndDeclareModule

Module Timer
  
  EnableExplicit
  
  #debugAlwaysFailHiResTimer = #False
  
  Prototype protoStart()
  Prototype.q protoGetNanoSeconds()
  
  Define prot_Start.protoStart
  Define prot_GetNanoSeconds.protoGetNanoSeconds
  
  Define *StartTimer, *GetTimerNanoSeconds
  
  Declare.l hiResTimerExists()
  Declare useStandardTimer()
  Declare useHiResTimer()
  Declare StartHiresTimer()
  Declare StartStandardTimer()
  Declare.q GetHiresTimer()
  Declare.q GetStandardTimer()
  
  Global.q frequency, counterStartValue, counter
  
  If hiResTimerExists() = #False
    useStandardTimer()
  Else
    useHiResTimer()
  EndIf
  
  prot_Start = *StartTimer
  prot_GetNanoSeconds = *GetTimerNanoSeconds
  
  Procedure Start()
    
    Shared prot_Start
    prot_Start()
    
  EndProcedure
  
  Procedure.q GetNanoSeconds()
    
    Shared prot_GetNanoSeconds
    ProcedureReturn prot_GetNanoSeconds()
    
  EndProcedure
  
  Procedure.l hiResTimerExists()
    
    QueryPerformanceFrequency_(@frequency)
    CompilerIf #debugAlwaysFailHiResTimer
      frequency = 0
    CompilerEndIf
    
    If frequency > 0
      Debug "Module Timer is choosing a high resolution timer"
      ProcedureReturn #True
    Else
      Debug "Module Timer can't find the high resolution timer, fallback to standard 1ms resolution timer."
      ProcedureReturn #False
    EndIf
    
  EndProcedure
  
  Procedure useStandardTimer()
    
    Shared *StartTimer, *GetTimerNanoSeconds
    
    *StartTimer = @StartStandardTimer()
    *GetTimerNanoSeconds = @GetStandardTimer()
    
  EndProcedure
  
  Procedure useHiResTimer()
    
    Shared *StartTimer, *GetTimerNanoSeconds
    
    *StartTimer = @StartHiresTimer()
    *GetTimerNanoSeconds = @GetHiresTimer()
    
  EndProcedure
  
  Procedure StartHiresTimer()
    
    QueryPerformanceCounter_(@counter)
    counterStartValue = counter
    
  EndProcedure
  
  Procedure StartStandardTimer()
    
    counterStartValue = ElapsedMilliseconds()
    
  EndProcedure
  
  Procedure.q GetHiresTimer()
    
    QueryPerformanceCounter_(@counter)
    ProcedureReturn Abs(counter - counterStartValue) / frequency * 10e9
    
  EndProcedure
  
  Procedure.q GetStandardTimer()
    
    ProcedureReturn (ElapsedMilliseconds() - counterStartValue) * 10e6
    
  EndProcedure
  
EndModule

CompilerIf #PB_Compiler_IsMainFile And #PB_Compiler_Debugger

  #testDelay = 500
  #loopSize = 1e7
  
  Declare LetTheGoodLoopRoll()
  
  ; ***** Test 1
  
  Timer::Start()
  Debug "Testing HiRes timer with delay " + Str(#testDelay) + " ms"
  Delay(#testDelay)
  Debug Timer::GetNanoSeconds()
  
  ; ***** Test 2 - Accumulated timing
  
  Debug " "
  Debug "Testing speed in a loop 1 with size " + Str(#loopSize)
  
  Timer::Start()
  For j = 1 To 5
    LetTheGoodLoopRoll()
  Next
  
  ; ***** Test 3 - Lap times
  
  Debug " "
  Debug "Testing speed in a loop 2 with size " + Str(#loopSize)
  For j = 1 To 5
    Timer::Start()
    LetTheGoodLoopRoll()
  Next
  
  Procedure LetTheGoodLoopRoll()
    
    For i = 1 To #loopSize
      g = Random(1e6, 1)
    Next
    Debug Timer::GetNanoSeconds()
    
  EndProcedure
  
CompilerEndIf