A nice new use for quads in PB v4.0

Share your advanced PureBasic knowledge/code with the community.
User avatar
netmaestro
PureBasic Bullfrog
PureBasic Bullfrog
Posts: 8451
Joined: Wed Jul 06, 2005 5:42 am
Location: Fort Nelson, BC, Canada

A nice new use for quads in PB v4.0

Post by netmaestro »

Code updated For 5.20+
Ever wanted to do some hi-performance timing? It's useful for games certainly but it is also good for many other tasks requiring a large number of events fired per second. Here is a way that doesn't perform very well:

Code: Select all

Procedure TimerProc()
  Shared i
  i-1
EndProcedure

OpenWindow(0,0,0,400,240,"I'm trying to go away in 1 second",#PB_Window_SystemMenu|#PB_Window_ScreenCentered)

StartDrawing(WindowOutput(0))
  DrawText(20,50,"But I can't manage it because ElapsedMilliseconds()",0,RGB(227,227,227))
  DrawText(20,80,"only updates once every 15 milliseconds",0,RGB(227,227,227))
  DrawText(20,110,"so I take 15 seconds to go away",0,RGB(227,227,227))
StopDrawing()

i=1000
time=ElapsedMilliseconds()
t=0
Repeat
  ev=WindowEvent()
  t1=ElapsedMilliseconds()
  If t1-t>1
    TimerProc()
    t=t1
  EndIf
  
Until ev=#PB_Event_CloseWindow Or i < 1

Debug (ElapsedMilliseconds()-time)/1000
It works this way because ElapsedMilliseconds() only gets updated 65 times per second. Here is some proof:

Code: Select all

; Proof that ElapsedMilliseconds() updates 65 times
; per second, not 1000 as one might expect
; Instead of a list of 1000 values of t, each 1 higher
; than the last, we get 65 changes, each about 15 higher
; than the last.
DisableDebugger
Dim listof_ts(1000)

t=ElapsedMilliseconds()
t1=0
t2=0
quit=0
i=0
While quit=0
  t1=ElapsedMilliseconds()
  If t1-t2 > 0            ; ems() has changed
    listof_ts(i)=t1       ; so store new value
    t2=t1
    i+1
  EndIf
  If t1-t>1000
    quit=1
  EndIf
Wend

EnableDebugger

numchanges=0
For i = 1 To 1000
  If listof_ts(i)<>0
    numchanges+1
    Debug listof_ts(i)
  EndIf
Next

Debug numchanges
So ElapsedMilliseconds() is good up to 65 events per second, what other options are there? Waitwindowevent(<ms>)? Same performance. Delay(ms)? Same performance.

Here is a better way:

Code: Select all

; This used to be state of the art in hi-res timing
; PBOSL's hi-res timer is based on this API
; It works pretty well but I'm not sure if it's safe in dual-core processors

Procedure TimerProc(a,b,c,d,e)
  Shared i
  i-1
EndProcedure

OpenWindow(0,0,0,320,240,"I go away in 1 second",#PB_Window_SystemMenu|#PB_Window_ScreenCentered)

timeSetEvent_(1,0,@TimerProc(),0,#TIME_PERIODIC) ;This will fire once per millisec

i=1000
time=ElapsedMilliseconds()

Repeat
  ev=WindowEvent()
Until ev=#PB_Event_CloseWindow Or i < 1

timeKillEvent_($10)

Debug (ElapsedMilliseconds()-time)/1000
But this way could be falling into doubt with the advent of the new dual-core processors. Here is something interesting to read:

http://msdn.microsoft.com/library/defau ... essors.asp

[edit] If anyone knows if the timeSetEvent_() API method is vulnerable to the dual-core hazards or not, I would appreciate some feedback to this topic.

And here is a little implementation of it using PB v4.0's new Quad type:

Code: Select all

; Want an event that fires 3500 times per millisecond? 
; Now with quads in PB v4 you can get it easily 

QueryPerformanceFrequency_(@maxfreq.q)
If maxfreq <> 0
  eventspermicrosecond.l=maxfreq/1000000  ; This evaluates to 3 on my machine 
  If eventspermicrosecond < 1             ; That's 3 million events per second 
    eventspermicrosecond = 1              
  EndIf
Else
  Debug "Can't do the test"
EndIf
                                      
DisableDebugger 
Procedure TimerProc() 
  Shared j 
  j+1 
EndProcedure 

OpenWindow(0,0,0,320,240,"I go away in 1 second",#PB_Window_SystemMenu|#PB_Window_ScreenCentered) 
j=0 
time=ElapsedMilliseconds() 
QueryPerformanceCounter_(@t.q) 
Repeat 
  ev=WindowEvent() 
  QueryPerformanceCounter_(@t1.q) ;The new standard in hi-res timing, 
  If t1-t>=eventspermicrosecond   ;this will fire once per microsecond 
    TimerProc()                   ;without fail - it is even capable of 
    t=t1                          ;higher resolution than that but actual 
  EndIf                           ;performance will be slowed by program overhead 
  
Until ev=#PB_Event_CloseWindow Or ElapsedMilliseconds()-time > 1000 

EnableDebugger 
Debug j   ; My actual measured performance is 950,000 events in 1 second 
          ; With debugger enabled it is around 600,000 
Achievable timing performance of one event each microsecond! That's cool.
Last edited by netmaestro on Tue Feb 28, 2006 9:51 pm, edited 3 times in total.
BERESHEIT
User avatar
Hades
Enthusiast
Enthusiast
Posts: 188
Joined: Tue May 17, 2005 8:39 pm

Post by Hades »

Thanks for sharing. :)
Shannara
Addict
Addict
Posts: 1808
Joined: Thu Oct 30, 2003 11:19 pm
Location: Emerald Cove, Unformed

Post by Shannara »

That is awesome, I have a dual core, and will let you know when I have the time to touch the computer again :)
thefool
Always Here
Always Here
Posts: 5875
Joined: Sat Aug 30, 2003 5:58 pm
Location: Denmark

Post by thefool »

notice: I do NOT have a multicore processor, but with my 3800+ i get:
960 713 events pr second!


(that is with running background programs like netlimiter, avast antivirus, msn, various others etc etc)
User avatar
Rescator
Addict
Addict
Posts: 1769
Joined: Sat Feb 19, 2005 5:05 pm
Location: Norway

Post by Rescator »

This is a fun little "test". (and PB quads rock :P)

A Delay(0) is used to avoid cpu hogging,
and at least on my system the HQ elapsed is always 1000 ms.
While the classic elapsed is 1002, so apparently some inaccurcy.
(not odd really as elapsed has a margin of error equal to 10ms or similar like stated earlier in this thread)

Now play around with the Delay() value,
try anything from 0 to 1000, the HQ elapsed variant is the one displaying the most accurate elapsed time as it has a precision of at least a million per second (depending on your hardware) and thus precision of 100000 per millisecond.

My HQ elapsed is just intended as a high quality slot in for the original elapsed function, but with high precision timer accuracy obviously.
You can probably shave off a fraction of a millisecond by
moving the If maxfreq=0 outside the procedure,
and do that earlier (at start of program), and make the maxfreq variable global. Basically just leaving QueryPerformanceCounter_(@t.q) inside it.

It's a shame we can't get macros to "return" anything, as a macro would be even faster. I do not think using prototype will speed this up,
as I'm guessing that PB calls procedures internally as CallFunctionFast or similar?

And obviously you have to test at program start if the hardware support HQ timers or not. QueryPerformanceFrequency_ will return 0 if HQ timers are not supported.

Code: Select all

Procedure.l ElapsedMillisecondsHQ()
 Static maxfreq.q
 Protected t.q
 If maxfreq=0
  QueryPerformanceFrequency_(@maxfreq)
  maxfreq=maxfreq/1000 ;we want millisecond precision, not seconds, setting it here saves speed later.
 EndIf
 QueryPerformanceCounter_(@t.q)
 ProcedureReturn t/maxfreq
EndProcedure

Debug "HQ"
DisableDebugger

starttime=ElapsedMillisecondsHQ()
Repeat
 Delay(0)
 endtime=ElapsedMillisecondsHQ()
Until (endtime-starttime)>999

EnableDebugger
Debug endtime-starttime

Debug "Classic"
DisableDebugger

starttime=ElapsedMilliseconds()
Repeat
 Delay(0)
 endtime=ElapsedMilliseconds()
Until (endtime-starttime)>999

EnableDebugger
Debug endtime-starttime
User avatar
netmaestro
PureBasic Bullfrog
PureBasic Bullfrog
Posts: 8451
Joined: Wed Jul 06, 2005 5:42 am
Location: Fort Nelson, BC, Canada

Post by netmaestro »

Update: I acquired a new computer this week, a dual-core AMD 3800+, and I tested my Transpetris game (available from announcements section) which uses TimeSetEvent_() as its hi-res timing component. It seems to work no differently than on my single-core machines, which could mean TimeSetEvent_() is ok for dual-core. I realize one application isn't a wide enough test to draw any conclusions from, but it looks promising.
BERESHEIT
User avatar
J. Baker
Addict
Addict
Posts: 2188
Joined: Sun Apr 27, 2003 8:12 am
Location: USA
Contact:

Post by J. Baker »

Hmmm, you can see my pc spec's in my sig. But I got 1005959. I also had a few things running. How accurate is this?
www.posemotion.com

PureBasic Tools for OS X: PureMonitor, plist Tool, Data Maker & App Chef


Even the vine knows it surroundings but the man with eyes does not.
User avatar
netmaestro
PureBasic Bullfrog
PureBasic Bullfrog
Posts: 8451
Joined: Wed Jul 06, 2005 5:42 am
Location: Fort Nelson, BC, Canada

Post by netmaestro »

You can cash it at the bank. Any number you got represents the true number of separate times the TimerProc() procedure got executed, as it adds 1 to the total each time. Most modern processors will produce results similar to yours, notwithstanding a few other apps running, so long as they aren't real cpu-hogs.
BERESHEIT
User avatar
Rescator
Addict
Addict
Posts: 1769
Joined: Sat Feb 19, 2005 5:05 pm
Location: Norway

Post by Rescator »

This uses timeBeginPeriod_() timeEndPeriod_() and timeGetTime_() and is my favourite timing system actually.

None of the multi cpu issues that the HQ one has.

Easy to use, one line at start of program and one at the end, plus a macro and that's it,
no other sourcecode changes needed if you are using ElapsedMilliseconds() currently.

I get on average 997ms as the last result,
it seems the loop in total eats up 3ms though it can vary from 995ms to 998ms.
The Delay() at the start is needed as this "program" is so small that the loop starts at once which is not normally the case in programs/games. This gives the program a chance to "start" before it enters the loop.

Code: Select all

;Program start
timeBeginPeriod_(1) ;Force 1ms timing, must have a matching timeEndPeriod_(1)

Macro ElapsedMilliseconds()
 timeGetTime_()
EndMacro

; Proof that ElapsedMilliseconds() updates 65 times
; per second, not 1000 as one might expect
; Instead of a list of 1000 values of t, each 1 higher
; than the last, we get 65 changes, each about 15 higher
; than the last.
DisableDebugger
Dim listof_ts(999)

t1.l=0
t2.l=0
i.l=0

Delay(1000) ;to avoid runtime/startup execution "lag"

t.l=ElapsedMilliseconds()
Repeat
  t1=ElapsedMilliseconds()
  If t1-t2 > 0            ; ems() has changed
    listof_ts(i)=t1       ; so store new value
    t2=t1
    i+1
  EndIf
  If t1-t>1000
   Break
  EndIf
ForEver

EnableDebugger

numchanges=0
For i = 0 To 999
  If listof_ts(i)<>0
    numchanges+1
    Debug listof_ts(i)
  EndIf
Next

Debug numchanges
timeEndPeriod_(1) ;must match timeBeginPeriod_(1) at top
;Program end

End
Post Reply