TimerEx, hi precision timer using QueryPerformanceCounter
You are (obviously) welcome to read all the posts in this thread. But I've made additional comments in post #26 that may be of interest to you.
I've posted this class many times in specific question threads, and I'm often referring to it, so I thought I'd post it here. It's just a high precision timer using the QueryPerformanceCounter API call.
I'm posting the entire CLS code (including the header stuff). So, to use it, open Notepad (or any ASCII editor), past it in, and save as TimerEx.cls. I've also attached in in the attached ZIP.
As a note, it's got the VB_PredeclaredId flag set to True. That can only be done to a CLS module with Notepad, and can't be done from the VB6 IDE. But, once done, it works perfectly. This flag auto instantiates the CLS module, using the CLS module's name as the object variable name. (Forms have this VB_PredeclaredId flag set to True by default. That's why you can use their names as an object variable that auto instantiates.)
With this VB_PredeclaredId = True, there's no need to declare any variable. Just start using the TimerEx, and it'll just work. It basically just becomes another keyword in the VB6 language.
There is only one read-only property (actually a function) named Value. However, it has it's Value.VB_UserMemId set to 0. This means it's the default. So you don't even have to say TimerEx.Value. You can just say TimerEx to get the value.
As a final note, if you use this for timing tests (which it's very good at), be sure to just get a value from TimerEx before you start your timings. Any CLS declared with New or with its VB_PredeclaredId flag set to True will be instantiated the first time you touch it, and not before. Therefore, you don't want your instantiation time interfering with anything, so "touch" it before using it in a critical way, and then it'll stay instantiated (unless you explicitly set it to Nothing).
Here's the code (with header info):
Code:
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
Persistable = 0 'NotPersistable
DataBindingBehavior = 0 'vbNone
DataSourceBehavior = 0 'vbNone
MTSTransactionMode = 0 'NotAnMTSObject
END
Attribute VB_Name = "TimerEx"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = True
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = False
'
' This class has VB_PredeclaredId = True.
' Also, the Value is set to the default property.
' Therefore, TimerEx will just act like a new built-in function.
' No need to declare an object variable or explicitly instantiate.
'
Option Explicit
'
Private Declare Function QueryPerformanceCounter Lib "kernel32" (lpPerformanceCount As Currency) As Long
Private Declare Function QueryPerformanceFrequency Lib "kernel32" (lpFrequency As Currency) As Long
'
Private cFreq As Currency
'
Private Sub Class_Initialize()
QueryPerformanceFrequency cFreq
End Sub
Public Function Value() As Double
Attribute Value.VB_UserMemId = 0
' Must be Public (not Friend) so we can set default property.
' Returns seconds, a double, with high-precision.
' Caller is responsible for keeping track of start/stop times.
' Basically, it works exactly like the built-in Timer function, but with much more precision.
Dim cValue As Currency
QueryPerformanceCounter cValue
Value = cValue / cFreq ' The division will return a double, and it also cancels out the Currency decimal points.
End Function
And, the attachment is below.
Enjoy,
Elroy
EDIT1: Just as a notice, this TimerEx returns the number of seconds elapsed since the system on which it's running was last booted. And, AFAIK, it never rolls over. A Double can easily hold enough seconds that your computer would crumble to dust before it would need to roll over. This is in contrast to the built-in Timer function, which is pegged at midnight, and rolls over each day. Typically, I only use these things for Deltas (differences) so I don't care about their zero basis.
Last edited by Elroy; Sep 17th, 2019 at 07:55 AM.
Any software I post in these forums written by me is provided "AS IS" without warranty of any kind, expressed or implied, and permission is hereby granted, free of charge and without restriction, to any person obtaining a copy. To all, peace and happiness.
Re: TimerEx, hi precision timer using QueryPerformanceCounter
I'm wondering if the performance optimization of caching cFreq is worth the whole class as the uncomplicated version can be implemented with a single function in a .bas file:
thinBasic Code:
Private Declare Function QueryPerformanceCounter Lib "kernel32" (lpPerformanceCount As Currency) As Long
Private Declare Function QueryPerformanceFrequency Lib "kernel32" (lpFrequency As Currency) As Long
Re: TimerEx, hi precision timer using QueryPerformanceCounter
Originally Posted by The trick
FYI, if you use VB_PredeclaredId = True then you have a small overhead because each access to an object translated to:
Code:
If cObj Is Nothing Then Set cObj = New cObj
Hmm, that's interesting. I suppose that's fine with me so long as the overhead is consistent.
-------------
Originally Posted by wqweto
I'm wondering if the performance optimization of caching cFreq is worth the whole class as the uncomplicated version can be implemented with a single function in a .bas file:
thinBasic Code:
Private Declare Function QueryPerformanceCounter Lib "kernel32" (lpPerformanceCount As Currency) As Long
Private Declare Function QueryPerformanceFrequency Lib "kernel32" (lpFrequency As Currency) As Long
Public Function TimerEx() As Double
Dim cFreq As Currency
Dim cValue As Currency
QueryPerformanceFrequency cFreq
QueryPerformanceCounter cValue
TimerEx = cValue / cFreq
End Function
cheers,
</wqw>
And that's also an interesting question. I guess I'll have to use QueryPerformanceCounter and QueryPerformanceFrequency to test themselves. Shouldn't be difficult though.
----------
Taking both of the above together though, I suppose in an ideally optimized situation, we'd keep it all in a BAS module and also have an "InitTimerEx" call that was mandated before "TimerEx" was called. We could check in the "TimerEx" procedure, but, again, that'd be some unwanted overhead.
Maybe I'll take a closer look at this stuff later,
Elroy
Any software I post in these forums written by me is provided "AS IS" without warranty of any kind, expressed or implied, and permission is hereby granted, free of charge and without restriction, to any person obtaining a copy. To all, peace and happiness.
Re: TimerEx, hi precision timer using QueryPerformanceCounter
Okay, the verdict is in. There are four modules in all to do this testing, and that project is attached. However, the testing itself is all in Form1. Here's that code:
Code:
Option Explicit
'
Private Declare Function GetModuleHandle Lib "kernel32.dll" Alias "GetModuleHandleW" (ByVal lpModuleName As Long) As Long
'
Private Const Iterations = 2000000
Private ManualClsTimerEx As ManualClsTimerEx
'
Private Sub Form_Load()
Text1.FontName = "Courier New"
Text1.FontSize = 12
'
Set ManualClsTimerEx = New ManualClsTimerEx
'
Text1.Text = "CLICK THE FORM TO DO TIMINGS." & vbCrLf & _
" In The IDE: " & InIde6 & vbCrLf & _
"Instantiate: " & AutoClsTimerEx & vbCrLf & _
"Instantiate: " & ManualClsTimerEx & vbCrLf & _
"---------------------" & vbCrLf
End Sub
Private Sub Form_Click()
' We'll just initialize the TimerExBasWithInit each time here (outside of any timings).
InitForBasTimerEx
'
Text1.Text = Text1.Text & _
" TimeUsingAutoClass: " & TimeUsingAutoClass & vbCrLf & _
" TimeUsingManualClass: " & TimeUsingManualClass & vbCrLf & _
" TimeUsingNoInitBasTimerEx: " & TimeUsingNoInitBasTimerEx & vbCrLf & _
"TimeUsingTimerExBasWithInit: " & TimeUsingTimerExBasWithInit & vbCrLf & _
"---------------------" & vbCrLf
End Sub
Private Function TimeUsingAutoClass() As Double
Dim i As Long
Dim d As Double
Dim dStart As Double
Dim dEnd As Double
'
dStart = AutoClsTimerEx
For i = 1 To Iterations
d = AutoClsTimerEx ' It's the timing of AutoClsTimerEx being tested.
Next i
dEnd = AutoClsTimerEx
'
TimeUsingAutoClass = dEnd - dStart
End Function
Private Function TimeUsingManualClass() As Double
Dim i As Long
Dim d As Double
Dim dStart As Double
Dim dEnd As Double
'
dStart = AutoClsTimerEx
For i = 1 To Iterations
d = ManualClsTimerEx ' It's the timing of ManualClsTimerEx being tested.
Next i
dEnd = AutoClsTimerEx
'
TimeUsingManualClass = dEnd - dStart
End Function
Private Function TimeUsingNoInitBasTimerEx() As Double
Dim i As Long
Dim d As Double
Dim dStart As Double
Dim dEnd As Double
'
dStart = AutoClsTimerEx
For i = 1 To Iterations
d = NoInitBasTimerEx ' It's the timing of NoInitBasTimerEx being tested.
Next i
dEnd = AutoClsTimerEx
'
TimeUsingNoInitBasTimerEx = dEnd - dStart
End Function
Private Function TimeUsingTimerExBasWithInit() As Double
Dim i As Long
Dim d As Double
Dim dStart As Double
Dim dEnd As Double
'
dStart = AutoClsTimerEx
For i = 1 To Iterations
d = TimerExBasWithInit ' It's the timing of TimerExBasWithInit being tested.
Next i
dEnd = AutoClsTimerEx
'
TimeUsingTimerExBasWithInit = dEnd - dStart
End Function
Private Function InIde6() As Boolean
InIde6 = (GetModuleHandle(StrPtr("vba6")) <> 0)
End Function
As you can see, I've tested four approaches to using these QueryPerformanceCounter and QueryPerformanceFrequency calls. Compiled, which is probably what we're most interested in, it turns out that we can see overhead in everything, and that a version that's in a BAS module and does an initialization call to QueryPerformanceFrequency (not calling it each time) is the fastest, as we'd expect. Next fastest is what wqweto recommended. Third fastest is the case where we use a CLS module but manually instantiate it (but that isn't consistent). And then last is the case presented in the OP. Here are a few compiled runs of my testing code:
And, just for completeness, here are a few runs while still in the IDE:
Code:
CLICK THE FORM TO DO TIMINGS.
In The IDE: True
Instantiate: 867841.39021802
Instantiate: 867841.390223125
---------------------
TimeUsingAutoClass: 0.323476716526784
TimeUsingManualClass: 0.32679523807019
TimeUsingNoInitBasTimerEx: 0.241708710440435
TimeUsingTimerExBasWithInit: 0.221975176711567
---------------------
TimeUsingAutoClass: 0.321620167931542
TimeUsingManualClass: 0.318047469481826
TimeUsingNoInitBasTimerEx: 0.241511422442272
TimeUsingTimerExBasWithInit: 0.231404518708587
---------------------
TimeUsingAutoClass: 0.31885193742346
TimeUsingManualClass: 0.311261639813893
TimeUsingNoInitBasTimerEx: 0.242311514331959
TimeUsingTimerExBasWithInit: 0.222751929541118
---------------------
TimeUsingAutoClass: 0.321575677837245
TimeUsingManualClass: 0.335889081121422
TimeUsingNoInitBasTimerEx: 0.26366492419038
TimeUsingTimerExBasWithInit: 0.235398049349897
---------------------
However, let's not forget that we're typically going to call any one of these twice, once at the start of whatever we're timing, and once at the end of it. Therefore, whatever differences there are in the above timings are going to be minuscule (several decimal places smaller) than whatever it is we'll be timing. And, there's no way to remove all of the overhead (making the above numbers zero). Therefore, we just have to make sure that whatever it is we're checking for time is done many more times than calls to any of these timer functions.
Best Regards,
Elroy
Any software I post in these forums written by me is provided "AS IS" without warranty of any kind, expressed or implied, and permission is hereby granted, free of charge and without restriction, to any person obtaining a copy. To all, peace and happiness.
Re: TimerEx, hi precision timer using QueryPerformanceCounter
Originally Posted by DEXWERX
The first call to your "Global" TimerEx function will instantiate the class - and add that overhead only to the first call.
Dex, I thought Trick was suggesting that there's an "If cls Is Nothing..." check done on each call when VB_PredeclaredId=True.
However, the closeness of the timings for the first two methods (TimeUsingAutoClass and TimeUsingManualClass) (both compiled and in-IDE) suggest that that's possibly not the case. But, knowing Trick, he's possibly examined the machine code. If it is doing such a test, it's quite fast.
Elroy
Any software I post in these forums written by me is provided "AS IS" without warranty of any kind, expressed or implied, and permission is hereby granted, free of charge and without restriction, to any person obtaining a copy. To all, peace and happiness.
Re: TimerEx, hi precision timer using QueryPerformanceCounter
Also, just as an aside, all of this pretty conclusively shows that there's some overhead in making a call into an instantiated CLS as compared to making a call into a BAS (in general).
Any software I post in these forums written by me is provided "AS IS" without warranty of any kind, expressed or implied, and permission is hereby granted, free of charge and without restriction, to any person obtaining a copy. To all, peace and happiness.
Re: TimerEx, hi precision timer using QueryPerformanceCounter
My results:
CLICK THE FORM TO DO TIMINGS.
In The IDE: False
Instantiate: 30717,4133387763
Instantiate: 30717,4133417096
---------------------
TimeUsingAutoClass: 2,38058461340916
TimeUsingManualClass: 2,38982265902814
TimeUsingNoInitBasTimerEx: 4,44215207519301
TimeUsingTimerExBasWithInit: 2,26196890946085
---------------------
If i use the declared APIs in TLB i get:
CLICK THE FORM TO DO TIMINGS.
In The IDE: False
Instantiate: 31208,6440042659
Instantiate: 31208,6440076881
---------------------
TimeUsingAutoClass: 2,37273892351732
TimeUsingManualClass: 2,37825896866343
TimeUsingNoInitBasTimerEx: 4,42636731763923
TimeUsingTimerExBasWithInit: 2,25509121969299
---------------------
Just make small optimizations:
CLICK THE FORM TO DO TIMINGS.
In The IDE: False
Instantiate: 31719,6791920482
Instantiate: 31719,6791935148
---------------------
TimeUsingAutoClass: 2,33123616269586
TimeUsingManualClass: 2,32856242902199
TimeUsingNoInitBasTimerEx: 2,2230386822921
TimeUsingTimerExBasWithInit: 2,19657854559773
---------------------
Optimizations:
Code:
Option Explicit
'
'Private Declare Function QueryPerformanceCounter Lib "kernel32" (lpPerformanceCount As Currency) As Long
'Private Declare Function QueryPerformanceFrequency Lib "kernel32" (lpFrequency As Currency) As Long
'
Dim mdFreq As Double
Public Function NoInitBasTimerEx() As Double
Static cFreq As Currency
Static dFreq As Double
Static bIsInit As Boolean
Dim cValue As Currency
If cFreq = 0 Then
QueryPerformanceFrequency cFreq
dFreq = cFreq
End If
QueryPerformanceCounter cValue
NoInitBasTimerEx = cValue / dFreq
End Function
Public Sub InitForBasTimerEx()
Dim cFreq As Currency
QueryPerformanceFrequency cFreq
mdFreq = cFreq
End Sub
Public Function TimerExBasWithInit() As Double
Dim cValue As Currency
QueryPerformanceCounter cValue
TimerExBasWithInit = cValue / mdFreq
End Function
------------------------------------
'
' This class has VB_PredeclaredId = True.
' Also, the Value is set to the default property.
' Therefore, TimerEx will just act like a new built-in function.
' No need to declare an object variable or explicitly instantiate.
'
Option Explicit
'
'Private Declare Function QueryPerformanceCounter Lib "kernel32" (lpPerformanceCount As Currency) As Long
'Private Declare Function QueryPerformanceFrequency Lib "kernel32" (lpFrequency As Currency) As Long
'
Private dFreq As Double
'
Private Sub Class_Initialize()
Dim cFreq As Currency
QueryPerformanceFrequency cFreq
dFreq = cFreq
End Sub
Public Function Value() As Double
' Must be Public (not Friend) so we can set default property.
' Returns seconds, a double, with high-precision.
' Caller is responsible for keeping track of start/stop times.
' Basically, it works exactly like the built-in Timer function, but with much more precision.
Dim cValue As Currency
QueryPerformanceCounter cValue
Value = cValue / dFreq ' The division will return a double, and it also cancels out the Currency decimal points.
End Function
Re: TimerEx, hi precision timer using QueryPerformanceCounter
Originally Posted by Elroy
Dex, I thought Trick was suggesting that there's an "If cls Is Nothing..." check done on each call when VB_PredeclaredId=True.
He is (correctly) suggesting that. That code that does the test, has very little to no overhead on a modern cpu.
The problem is the first time it's called, you have the overhead of instantiating the class...
Re: TimerEx, hi precision timer using QueryPerformanceCounter
Elroy, when you declare a class with the VB_PredeclaredId attribute or you have a declaration looks like Dim cObj As New Class you'll have checking like:
The original code:
Code:
cObj.Method1(x, y, z);
cObj.Method2(x, y, z);
If ObjPtr(cObj) Then
...
End if
is compiled to:
Code:
If cObj Is Nothing Then Set cObj = New Class
cObj.Method1(x, y, z);
If cObj Is Nothing Then Set cObj = New Class
cObj.Method2(x, y, z);
If cObj Is Nothing Then Set cObj = New Class
If ObjPtr(cObj) Then
...
End if
Re: TimerEx, hi precision timer using QueryPerformanceCounter
Originally Posted by wqweto
Btw, does this TimerEx version has the midnight problem inherent to built-in Timer, i.e. this
thinBasic Code:
Dim dblTimer As Double
dblTimer = Timer
Do While Timer < dblTimer + DURATION
DoEvent
Loop
. . . is not working around midnight (will loop forever)?
cheers,
</wqw>
From MSDN:
How often does QPC roll over?
Not less than 100 years from the most recent system boot, and potentially longer based on the underlying hardware timer used. For most applications, rollover isn't a concern.
Re: TimerEx, hi precision timer using QueryPerformanceCounter
Trick, please feel free to post your declarations TypeLib. If people are worried about these overheads, they could use it to build a TimerEx which would return in the fastest possible way.
Personally, I'm still thinking we're picking at nits. I seriously doubt that calls to any TimerEx are going to be within any tight loop. And, even if they are, I seriously doubt that the differences we're looking at are going to have any effect that's noticeable to within 4 or 5 (maybe more) decimal places.
Truth be told, I do use my TimerEx class within a long loop in some production code. Specifically, it's to slow down the "playing" of "frames" so that it matches the framerate at which the data were collected. Even here though, I'm typically talking about a framerate that's probably 240Hz at the fastest (much faster than the GPU).
So, that's a speed of 0.004166 seconds per frame. Let's think about how long it's taking to make a single call to TimerEx (using the slowest method). Just picking one of the above, we've got about 0.163434998015873 seconds to call it 2000000 times. Therefore, it takes about 0.0000000817 seconds to make the TimerEx call.
So, if we divide 0.0000000817 by 0.004166, we get about 0.0000196, or 0.00196%, of our frame time making a call to TimerEx to check the timing. That's a pretty small number, a number I'm willing to live with as minuscule overhead in a long loop.
Best Regards,
Elroy
EDIT1: Just as an FYI, I do see a distinction between a "tight loop" and a "long loop". To me, a "tight loop" should be one we want to run as fast as possible. And, in my case, a "long loop" is a loop we wish to run at a specific speed, typically much slower than the CPU would be capable of. Again, I can't think of a reason we'd want a TimerEx in a "tight loop", as we've done above to check the actual performances of the TimerEx function.
Last edited by Elroy; Oct 26th, 2018 at 10:37 AM.
Any software I post in these forums written by me is provided "AS IS" without warranty of any kind, expressed or implied, and permission is hereby granted, free of charge and without restriction, to any person obtaining a copy. To all, peace and happiness.
Re: TimerEx, hi precision timer using QueryPerformanceCounter
@Elroy: Very good!
I've been using my custom Timer replacement called DateTimer just to overcome this built-in Timer limitation. I just made it so the returned value is Timer + Date * (24 * 60 * 60) i.e. the added part is the number of seconds since 1900-01-01.
It's using a single API call but has much less precision than TimerEx (only 5-6 digits past the floating point) and (just noticed) does not account for daylight saving time (we are changing back to astronomical time again this Sunday here in Europe).
Re: TimerEx, hi precision timer using QueryPerformanceCounter
Originally Posted by wqweto
Btw, does this TimerEx version has the midnight problem inherent to built-in Timer
Hey, thanks for the compliment.
And, no, the TimerEx (i.e., anything using QueryPerformanceCounter) shouldn't have the midnight problem. The TimerEx (with QueryPerformanceCounter divided by frequency) returns the number of seconds elapsed since the system was last booted. And, AFAIK, it never rolls over.
And, as you know, the built-in Timer is just pegged to seconds past midnight.
I only use these things to check Deltas (i.e., differences) so I never care about their zero basis. But that's a good point for people to know. I'll put a small notice in the OP about that.
Best Regards,
Elroy
Any software I post in these forums written by me is provided "AS IS" without warranty of any kind, expressed or implied, and permission is hereby granted, free of charge and without restriction, to any person obtaining a copy. To all, peace and happiness.
Re: TimerEx, hi precision timer using QueryPerformanceCounter
Personally, I'm still thinking we're picking at nits. I seriously doubt that calls to any TimerEx are going to be within any tight loop. And, even if they are, I seriously doubt that the differences we're looking at are going to have any effect that's noticeable to within 4 or 5 (maybe more) decimal places.
I just told about how the VB6 does generally such things under the hood. These things are the general ones and don't affect (nearly) within the current situation/tests. I'm very embarrassed and i don't want to hijack the current thread to provide the more significant tests but if you don't mind i show the more significant case (and exactly attached the TLB ).
Once i had the discussion about the VB6 performance on the russian forum and we tested the Rnd function. I wrote the test of the random generation with the module, the class and the pure-VB-Rnd function. Of course all the analogs don't implement all the functionality of the Pure-VB-Rnd function (like the thread-independence, repeating etc.) but make the main task - the generation of random numbers between 0...1. That's code:
The main form:
Code:
Option Explicit
Private Const ITERATIONS = 20000000
Private Sub Form_Click()
Dim fR1() As Single
Dim fR2() As Single
Dim fR3() As Single
Dim fRes1 As Currency
Dim fRes2 As Currency
Dim fRes3 As Currency
Dim lIndex As Long
Dim cRndObj As New CRnd
Dim cFreq As Currency
Dim cValue As Currency
Dim cValue2 As Currency
ReDim fR1(ITERATIONS - 1)
ReDim fR2(ITERATIONS - 1)
ReDim fR3(ITERATIONS - 1)
QueryPerformanceFrequency cFreq
QueryPerformanceCounter cValue
Set cRndObj = New CRnd
For lIndex = 0 To ITERATIONS - 1
fR1(lIndex) = cRndObj.MyRnd()
Next
QueryPerformanceCounter cValue2
fRes1 = (cValue2 - cValue) / cFreq
modRnd.InitRnd
For lIndex = 0 To ITERATIONS - 1
fR2(lIndex) = modRnd.MyRnd()
Next
QueryPerformanceCounter cValue
fRes2 = (cValue - cValue2) / cFreq
For lIndex = 0 To ITERATIONS - 1
fR3(lIndex) = Rnd()
Next
QueryPerformanceCounter cValue2
fRes3 = (cValue2 - cValue) / cFreq
MsgBox "Obj " & Format$(fRes1, "0.00000") & vbNewLine & _
"Mod " & Format$(fRes2, "0.00000") & vbNewLine & _
"VBRnd " & Format$(fRes3, "0.00000")
For lIndex = 0 To ITERATIONS - 1
' // Checking
If fR2(lIndex) <> fR1(lIndex) Or _
fR2(lIndex) <> fR3(lIndex) Then Stop
Next
End Sub
CRnd class:
Code:
Option Explicit
Dim oldVal As Long
Public Function MyRnd() As Single
Dim bIsInIde As Boolean
Debug.Assert MakeTrue(bIsInIde)
If Not bIsInIde Then
oldVal = (&HFFC39EC3 - oldVal * &H2BC03) And &HFFFFFF
Else
' // This code won't added to EXE
GetMem4 CCur(429101.0243@ - (CCur(oldVal / 10000) * 179203@)), oldVal
oldVal = oldVal And &HFFFFFF
End If
MyRnd = oldVal / 16777216
End Function
Private Sub Class_Initialize()
oldVal = 327680
End Sub
modRnd.bas module:
Code:
Option Explicit
Dim oldVal As Long
Public Function MyRnd() As Single
Dim bIsInIde As Boolean
Debug.Assert MakeTrue(bIsInIde)
If Not bIsInIde Then
oldVal = (&HFFC39EC3 - oldVal * &H2BC03) And &HFFFFFF
Else
' // This code won't added to EXE
GetMem4 CCur(429101.0243@ - (CCur(oldVal / 10000) * 179203@)), oldVal
oldVal = oldVal And &HFFFFFF
End If
MyRnd = oldVal / 16777216
End Function
Public Sub InitRnd()
oldVal = 327680
End Sub
Public Function MakeTrue( _
ByRef bIsInIde As Boolean) As Boolean
bIsInIde = True
MakeTrue = True
End Function
Re: TimerEx, hi precision timer using QueryPerformanceCounter
Originally Posted by The trick
I'm very embarrassed and i don't want to hijack the current thread...
Trick,
You hijack any of my threads anytime you want. I'd give both you and Wqweto stars for your contributions to this, but I need to spread some around first.
I'm still attracted to the simplicity and elegance of the class in the OP, although you've clearly shown that there are alternatives with less overhead. And, I don't think we've identified anything in that OP that would be called a bug.
Trick, have no doubt that I am extremely impressed with your understanding of VB6, at all levels! YOU are the one who should knock out an open-source version of the VB6 IDE and compiler. Heck, forget the linker. It works fine as it is.
You Take Care,
Elroy
Any software I post in these forums written by me is provided "AS IS" without warranty of any kind, expressed or implied, and permission is hereby granted, free of charge and without restriction, to any person obtaining a copy. To all, peace and happiness.
Re: TimerEx, hi precision timer using QueryPerformanceCounter
Hi Couttsj,
According to Microsoft, QueryPerformanceCounter can have a resolution of up to ONE millisecond. And GetTickCount can get no better than about 10 milliseconds (or ONE centisecond). Also, GetTickCount supposedly wraps after 49.7 days. Whereas, from any reference I can find, I don't believe that QueryPerformanceCounter ever wraps.
Best Regards,
Elroy
EDIT1: With the understanding that a Double gives us 15 digits of base-10 precision, and that we need four for our microseconds, that leaves 11 digits for whole-seconds. So, we could count up to 99,999,999,999 seconds before we needed to wrap. That's over 3,000 years, so I don't think we need to worry about wrapping.
Last edited by Elroy; Oct 26th, 2018 at 05:14 PM.
Any software I post in these forums written by me is provided "AS IS" without warranty of any kind, expressed or implied, and permission is hereby granted, free of charge and without restriction, to any person obtaining a copy. To all, peace and happiness.
Re: TimerEx, hi precision timer using QueryPerformanceCounter
Hmmm, I thought about also developing a UnixTime high-precision timer (pegged at January 1, 1970), so we'd know what ZERO meant.
However, apparently, a good UnixTime function accounts for these things called "leap seconds", little adjustments we occasionally make to the clock to keep the noon sun at the top of the sky. Also, it'd be dependent upon the accuracy of the system clock, which I know can have inaccuracies.
Therefore, I think I'll let that one go.
Any software I post in these forums written by me is provided "AS IS" without warranty of any kind, expressed or implied, and permission is hereby granted, free of charge and without restriction, to any person obtaining a copy. To all, peace and happiness.
Re: TimerEx, hi precision timer using QueryPerformanceCounter
Originally Posted by Elroy
Hi Couttsj,
According to Microsoft, QueryPerformanceCounter can have a resolution of up to ONE millisecond. And GetTickCount can get no better than about 10 milliseconds (or ONE centisecond). Also, GetTickCount supposedly wraps after 49.7 days. Whereas, from any reference I can find, I don't believe that QueryPerformanceCounter ever wraps.
Best Regards,
Elroy
EDIT1: With the understanding that a Double gives us 15 digits of base-10 precision, and that we need four for our microseconds, that leaves 11 digits for whole-seconds. So, we could count up to 99,999,999,999 seconds before we needed to wrap. That's over 3,000 years, so I don't think we need to worry about wrapping.
GetTickCount supposedly relies on the system timer. Using Clockres.exe from SysInternals yields:
------------------------------------------------
Clockres v2.1 - Clock resolution display utility
Copyright (C) 2016 Mark Russinovich
Sysinternals
Maximum timer interval: 15.625 ms
Minimum timer interval: 0.500 ms
Current timer interval: 15.625 ms
------------------------------------------------
Using timeBeginPeriod 1/timeEndPeriod 1, I can reduce that Interval to 1.001 ms. But Win 8+ appears to use it inconsistently. So indeed your timer seems to be more precise.
Re: TimerEx, hi precision timer using QueryPerformanceCounter
For demonstration purposes, here is my version:
Code:
Option Explicit
Private Declare Sub Sleep Lib "kernel32.dll" (ByVal dwMilliseconds As Long)
Private Declare Function timeBeginPeriod Lib "winmm.dll" (ByVal uPeriod As Long) As Long
Private Declare Function timeEndPeriod Lib "winmm.dll" (ByVal uPeriod As Long) As Long
Private Declare Function QueryPerformanceCounter Lib "kernel32" (lpPerformanceCount As Currency) As Long
Private Declare Function QueryPerformanceFrequency Lib "kernel32" (lpFrequency As Currency) As Long
Private Function TimerEx() As Double
Dim cFreq As Currency
Dim cValue As Currency
QueryPerformanceFrequency cFreq
QueryPerformanceCounter cValue
TimerEx = cValue / cFreq
End Function
Private Sub cmdStart_Click()
Dim Start As Double
Static Delay As Long
Dim Result(4) As Long
Dim N%
Delay = Delay + CLng(Text2.Text)
Text1.Text = CStr(Delay) & " ms Delay" & vbCrLf
timeBeginPeriod 1
Start = TimerEx
Sleep Delay
Result(0) = CLng((TimerEx - Start) * 1000)
Sleep Delay
Result(1) = CLng((TimerEx - Start) * 1000)
Sleep Delay
Result(2) = CLng((TimerEx - Start) * 1000)
Sleep Delay
Result(3) = CLng((TimerEx - Start) * 1000)
Sleep Delay
Result(4) = CLng((TimerEx - Start) * 1000)
timeEndPeriod 1
For N% = 0 To 4
Text1.Text = Text1.Text & Result(N%) & vbCrLf
Next N%
End Sub
The delay is provided by the Sleep function, which has been enhanced using "timeBeginPeriod 1" and "timeEndPeriod 1". This modifies the System Timer Interval to 1.001, and is a system wide modification. As long as you use "timeEndPeriod" with the same interval as "timeBeginPeriod", the System Timer is restored to it's original setting. Each time the Start button is depressed, the delay is incremented by the ms box.
10 ms Delay
10 + 11 =
21 + 10 =
31 + 11 =
42 + 11 =
53
20 ms Delay
20 + 20 =
40 + 20 =
60 + 21 =
81 + 20 =
101
30 ms Delay
31 + 30 =
61 + 31 =
92 + 31 =
123 + 30 =
153
40 ms Delay
40 + 41 =
81 + 41 =
122 + 41 =
163 + 41 =
204
50 ms Delay
50 + 50 =
100 + 51 =
151 + 51 =
202 + 50 =
252
Re: TimerEx, hi precision timer using QueryPerformanceCounter
Thanks for the great code and advice in here.
But, I am totally confused.
I am exploring the code attached to Post#6 by Elroy, and am interested in the last option (TimeUsingTimerExBasWithInit) because it is the fastest of all:
I am trying to simplify it (and extract the minimum code) for my use in my bas module, but there is some confusion.
As far as I understand, this technique of time calculation involves three declarations in a bas module (two API and one variable declaration):
Code:
Private Declare Function QueryPerformanceCounter Lib "kernel32" (lpPerformanceCount As Currency) As Long
Private Declare Function QueryPerformanceFrequency Lib "kernel32" (lpFrequency As Currency) As Long
Dim mcFreq As Currency
And one functions (in the same bas module) to be called only once when the application starts (for example in the MDIForm_Load):
Code:
Public Sub InitForBasTimerEx()
QueryPerformanceFrequency mcFreq
End Sub
And another functions (also in the same bas module) to be called as many times as the programmer wishes to get the time since the application started (or the PC was rebooted):
Code:
Public Function TimerExBasWithInit() As Double
Dim cValue As Currency
QueryPerformanceCounter cValue
TimerExBasWithInit = cValue / mcFreq
End Function
What I understand from all of this is that the bas module variable "mcFreq" should be declared as Public.
What I understand from all of this is that "mcFreq" should be available to the whole application, such that it is SET once (by a call to InitForBasTimerEx) when the application starts (for example in the MDIForm_Load), and then USED numerous times by any later calls to "TimerExBasWithInit"
But, I see that in that code, it is declared as Dim instead of Public.
I set a breakpoint in that project's Form_Load, and check the value of "mcFreq" before and after the call to:
Code:
Set ManualClsTimerEx = New ManualClsTimerEx
in the Immediate window, and it shows the value as blank, and not a numeric value.
Can anybody shed some light to this matter please?
I am trying to EXTRACT the minumum code to add to my project for time calculation.
Can I assume that the above three little snippets that I have extracted (the declarations and the two functions) are all I need to put in my bas module?
Re: TimerEx, hi precision timer using QueryPerformanceCounter
Ilia, and all,
Several people are saying several things in this thread. However, if you want simple, just take what I posted in post #1, make a class out of it, and just use it (no explicit instantiation needed) as TimerEx.
Now, there are a couple of caveats:
It's probably best to make the TimerEx call when your program starts up and then just toss the results. That will make sure it's instantiated and that you don't have that instantiation work_delay later on. It should stay instantiated for the life of your project unless you explicitly uninstantiate it (which would be: Set TimerEx = Nothing).
On that first call to TimerEx, don't have it in the same procedure where you'll actually be using it for any timings.
Yes, there is a touch of overhead (but minuscule) to calling TimerEx (that could be reduced, see The Trick's post #3 above), but it should be the same for each call. Therefore, if whatever your timing is more than a couple of seconds (or even less), then that overhead should be entirely negligible. And, there's always going to be a few CPU cycles of overhead regardless. We can't make API calls and have time stop while we do it.
Within those parameters, that class has served my purposes quite well. Actually, I use it for more than just testing the speed of code. I also use it for things such as timing the framerate when doing DirectX work, and it serves me quite well.
And, as a final note about that overhead ... it doesn't distort the reporting of TimerEx. It simply gets added to the reported time (or time elapsed). So, using it to test framerates works perfectly. And, say you call it twice to test the timing of some code in a loop, with a TimerEx call above and below the loop, in that case you will just have a fixed amount of minuscule built into your elapsed time.
Good Luck,
Elroy
EDIT1: And dilettante recently said something relevant in a another thread. Basically, in this age of potentially massive multi-tasking operating systems (like Windows 10), we will never get precise and consistent timings. For whatever CPU core our thread is running in, we will always be competing for CPU core timeslices with who knows what other processes and services.
Last edited by Elroy; Sep 17th, 2019 at 08:08 AM.
Any software I post in these forums written by me is provided "AS IS" without warranty of any kind, expressed or implied, and permission is hereby granted, free of charge and without restriction, to any person obtaining a copy. To all, peace and happiness.
Re: TimerEx, hi precision timer using QueryPerformanceCounter
This version is a little quicker than the class module, two times in the debugger and five times compiled.
Reports in milliseconds, which you might round to two or three places.
Copy to a module or save as .bas.
Code:
Option Explicit
Private Declare Function QueryPerformanceCounter Lib "kernel32" (lpPerformanceCount As Currency) As Long
Private Declare Function QueryPerformanceFrequency Lib "kernel32" (lpFrequency As Currency) As Long
Dim Clock1 As Currency
Dim Clock2 As Currency
Dim Freq As Double
Dim x As Long
Public Sub Start()
If x = 0 Then x = 2147483647: QueryPerformanceFrequency Clock1: Freq = 1000 / Clock1
QueryPerformanceCounter Clock1
End Sub
Public Function Finish() As Double
QueryPerformanceCounter Clock2
Finish = (Clock2 - Clock1) * Freq
End Function
You can return arbitrary units, e.g.,
Milliseconds
Code:
If x = 0 Then x = 2147483647: QueryPerformanceFrequency Clock1: Freq = 1000 / Clock1
Seconds
Code:
If x = 0 Then x = 2147483647: QueryPerformanceFrequency Clock1: Freq = 1 / Clock1
Minutes
Code:
If x = 0 Then x = 2147483647: QueryPerformanceFrequency Clock1: Freq = (1/60) / Clock1
Hours
Code:
If x = 0 Then x = 2147483647: QueryPerformanceFrequency Clock1: Freq = (1/3600) / Clock1
Microsecond(µs) measurements are possible (with Freq = 1000000 / Clock1) but if your performance counter is lower than 2 mhz you won't have absolute accuracy.
You can check the performance counter and with CPUZ or WinTimerTester.