Results 1 to 26 of 26

Thread: TimerEx, hi precision timer using QueryPerformanceCounter

  1. #1

    Thread Starter
    PowerPoster Elroy's Avatar
    Join Date
    Jun 2014
    Location
    Near Nashville TN
    Posts
    9,936

    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.
    Attached Files Attached Files
    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.

  2. #2
    PowerPoster wqweto's Avatar
    Join Date
    May 2011
    Location
    Sofia, Bulgaria
    Posts
    5,156

    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:
    1. Private Declare Function QueryPerformanceCounter Lib "kernel32" (lpPerformanceCount As Currency) As Long
    2. Private Declare Function QueryPerformanceFrequency Lib "kernel32" (lpFrequency As Currency) As Long
    3.  
    4. Public Function TimerEx() As Double
    5.     Dim cFreq As Currency
    6.     Dim cValue As Currency
    7.     QueryPerformanceFrequency cFreq
    8.     QueryPerformanceCounter cValue
    9.     TimerEx = cValue / cFreq
    10. End Function
    cheers,
    </wqw>

  3. #3

  4. #4

    Thread Starter
    PowerPoster Elroy's Avatar
    Join Date
    Jun 2014
    Location
    Near Nashville TN
    Posts
    9,936

    Re: TimerEx, hi precision timer using QueryPerformanceCounter

    Quote Originally Posted by The trick View Post
    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.

    -------------

    Quote Originally Posted by wqweto View Post
    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:
    1. Private Declare Function QueryPerformanceCounter Lib "kernel32" (lpPerformanceCount As Currency) As Long
    2. Private Declare Function QueryPerformanceFrequency Lib "kernel32" (lpFrequency As Currency) As Long
    3.  
    4. Public Function TimerEx() As Double
    5.     Dim cFreq As Currency
    6.     Dim cValue As Currency
    7.     QueryPerformanceFrequency cFreq
    8.     QueryPerformanceCounter cValue
    9.     TimerEx = cValue / cFreq
    10. 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.

  5. #5
    PowerPoster
    Join Date
    Jun 2015
    Posts
    2,224

    Re: TimerEx, hi precision timer using QueryPerformanceCounter

    The first call to your "Global" TimerEx function will instantiate the class - and add that overhead only to the first call.

  6. #6

    Thread Starter
    PowerPoster Elroy's Avatar
    Join Date
    Jun 2014
    Location
    Near Nashville TN
    Posts
    9,936

    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:

    Code:
     In The IDE:  False
    Instantiate: 867457.654884335
    Instantiate: 867457.654886158
    ---------------------
             TimeUsingAutoClass: 0.163434998015873
           TimeUsingManualClass: 0.166667456855066
      TimeUsingNoInitBasTimerEx: 0.124106141505763
    TimeUsingTimerExBasWithInit: 0.115294555202127
    ---------------------
             TimeUsingAutoClass: 0.163133778376505
           TimeUsingManualClass: 0.155467628850602
      TimeUsingNoInitBasTimerEx: 0.118451162124984
    TimeUsingTimerExBasWithInit: 0.114395637065172
    ---------------------
             TimeUsingAutoClass: 0.165043204557151
           TimeUsingManualClass: 0.159728099941276
      TimeUsingNoInitBasTimerEx: 0.121101238648407
    TimeUsingTimerExBasWithInit: 0.114371203933842
    ---------------------
             TimeUsingAutoClass: 0.162556136841886
           TimeUsingManualClass: 0.153970282757655
      TimeUsingNoInitBasTimerEx: 0.120634822174907
    TimeUsingTimerExBasWithInit: 0.10941129073035
    ---------------------

    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
    Attached Files Attached Files
    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.

  7. #7

    Thread Starter
    PowerPoster Elroy's Avatar
    Join Date
    Jun 2014
    Location
    Near Nashville TN
    Posts
    9,936

    Re: TimerEx, hi precision timer using QueryPerformanceCounter

    Quote Originally Posted by DEXWERX View Post
    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.

  8. #8

    Thread Starter
    PowerPoster Elroy's Avatar
    Join Date
    Jun 2014
    Location
    Near Nashville TN
    Posts
    9,936

    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.

  9. #9
    PowerPoster
    Join Date
    Feb 2015
    Posts
    2,687

    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

  10. #10
    PowerPoster
    Join Date
    Jun 2015
    Posts
    2,224

    Re: TimerEx, hi precision timer using QueryPerformanceCounter

    Quote Originally Posted by Elroy View Post
    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...


  11. #11
    PowerPoster wqweto's Avatar
    Join Date
    May 2011
    Location
    Sofia, Bulgaria
    Posts
    5,156

    Re: TimerEx, hi precision timer using QueryPerformanceCounter

    Btw, does this TimerEx version has the midnight problem inherent to built-in Timer, i.e. this

    thinBasic Code:
    1. Dim dblTimer As Double
    2. dblTimer = Timer
    3. Do While Timer < dblTimer + DURATION
    4.     DoEvent
    5. Loop
    . . . is not working around midnight (will loop forever)?

    cheers,
    </wqw>

  12. #12
    PowerPoster
    Join Date
    Feb 2015
    Posts
    2,687

    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

  13. #13
    PowerPoster
    Join Date
    Feb 2015
    Posts
    2,687

    Re: TimerEx, hi precision timer using QueryPerformanceCounter

    Quote Originally Posted by wqweto View Post
    Btw, does this TimerEx version has the midnight problem inherent to built-in Timer, i.e. this

    thinBasic Code:
    1. Dim dblTimer As Double
    2. dblTimer = Timer
    3. Do While Timer < dblTimer + DURATION
    4.     DoEvent
    5. 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.

  14. #14

    Thread Starter
    PowerPoster Elroy's Avatar
    Join Date
    Jun 2014
    Location
    Near Nashville TN
    Posts
    9,936

    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.

  15. #15
    PowerPoster wqweto's Avatar
    Join Date
    May 2011
    Location
    Sofia, Bulgaria
    Posts
    5,156

    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).

    cheers,
    </wqw>

  16. #16

    Thread Starter
    PowerPoster Elroy's Avatar
    Join Date
    Jun 2014
    Location
    Near Nashville TN
    Posts
    9,936

    Re: TimerEx, hi precision timer using QueryPerformanceCounter

    Quote Originally Posted by wqweto View Post
    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.

  17. #17
    PowerPoster
    Join Date
    Feb 2015
    Posts
    2,687

    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
    The results:
    IDE:
    ---------------------------
    Project1
    ---------------------------
    Obj 7,68400

    Mod 6,32100

    VBRnd 0,99810
    ---------------------------
    ОК
    ---------------------------
    EXE (with the optimizations):
    ---------------------------
    Project1
    ---------------------------
    Obj 1,03530

    Mod 0,15190

    VBRnd 1,44850
    ---------------------------
    ОК
    ---------------------------
    The std-module implementation is almost 7 times faster than Class implementation and almost 10 times! faster than pure-vb-rnd function.
    Attached Files Attached Files

  18. #18

    Thread Starter
    PowerPoster Elroy's Avatar
    Join Date
    Jun 2014
    Location
    Near Nashville TN
    Posts
    9,936

    Re: TimerEx, hi precision timer using QueryPerformanceCounter

    Quote Originally Posted by The trick View Post
    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.

  19. #19
    Frenzied Member
    Join Date
    Dec 2012
    Posts
    1,477

    Re: TimerEx, hi precision timer using QueryPerformanceCounter

    Silly question, but I have always used GetTickCount to get precision. Is QueryPerformanceCounter any more precise?

    J.A. Coutts

  20. #20

    Thread Starter
    PowerPoster Elroy's Avatar
    Join Date
    Jun 2014
    Location
    Near Nashville TN
    Posts
    9,936

    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.

  21. #21

    Thread Starter
    PowerPoster Elroy's Avatar
    Join Date
    Jun 2014
    Location
    Near Nashville TN
    Posts
    9,936

    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.

  22. #22
    Addicted Member
    Join Date
    Jun 2002
    Location
    Finland
    Posts
    169

    Re: TimerEx, hi precision timer using QueryPerformanceCounter

    Quote Originally Posted by Elroy View Post
    According to Microsoft, QueryPerformanceCounter can have a resolution of up to ONE millisecond.
    https://msdn.microsoft.com/fi-fi/lib...(v=vs.85).aspx
    Retrieves the current value of the performance counter, which is a high resolution (<1us) time stamp that can be used for time-interval measurements.

    So we're talking about nanoseconds

  23. #23
    Frenzied Member
    Join Date
    Dec 2012
    Posts
    1,477

    Re: TimerEx, hi precision timer using QueryPerformanceCounter

    Quote Originally Posted by Elroy View Post
    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.

    J.A. Coutts

  24. #24
    Frenzied Member
    Join Date
    Dec 2012
    Posts
    1,477

    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

    J.A. Coutts
    Attached Images Attached Images  

  25. #25
    Fanatic Member
    Join Date
    Mar 2010
    Posts
    764

    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:
    Code:
        Text1.Text = Text1.Text & _
            "         TimeUsingAutoClass: " & TimeUsingAutoClass & vbCrLf & _
            "       TimeUsingManualClass: " & TimeUsingManualClass & vbCrLf & _
            "  TimeUsingNoInitBasTimerEx: " & TimeUsingNoInitBasTimerEx & vbCrLf & _
            "TimeUsingTimerExBasWithInit: " & TimeUsingTimerExBasWithInit & vbCrLf & _
            "---------------------" & vbCrLf
    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?

    Then why "mcFreq" is not Public?

    Please help.
    Thanks.

  26. #26

    Thread Starter
    PowerPoster Elroy's Avatar
    Join Date
    Jun 2014
    Location
    Near Nashville TN
    Posts
    9,936

    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.

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •  



Click Here to Expand Forum to Full Width