Using Timers to Evaluate Code Performance
Pages: 1, 2
The High-Resolution Performance Counter
If the resolution offered by the multimedia timer is inadequate (as it is in the For Each loop of our previous code fragment), the performance timer used by the system for performance monitoring is also available. Since its resolution depends on the system (it is set when the system is started, and cannot be modified subsequently), you must first use the QueryPerformanceFrequency function to determine what the resolution of the system's performance timer is. Its syntax is:
Public Declare Function QueryPerformanceFrequency Lib "kernel32.dll" _
(ByRef lpFrequency As LARGE_INTEGER) As Boolean
The function returns a Boolean statement indicating whether the system supports a high-resolution performance counter.
Note that the function's single argument is a 64-bit integer that, when the function returns, contains the number of ticks per second. LARGE_INTEGER, the 64-bit integer, is actually a structure, defined as follows:
Public Type LARGE_INTEGER
lowpart As Long
highpart As Long
End Type
|
Related Reading
VBScript in a Nutshell |
Unfortunately, since Visual Basic in its pre-.NET versions doesn't offer native support for 64-bit integers, LARGE_INTEGER is cumbersome to work with. You can't simply assign it to an integer, or an overflow error results. You can't simply assign each of its members to a Single or Double, since the low-order integer will more often then not be interpreted as a negative number.
One viable suggestion (and certainly the easiest to implement) is to modify the function definition so that lpFrequency is of type Currency, which in Visual Basic is a 64-bit number. Because the decimal representation of currency values in Visual Basic has four decimal places, the value of lpFrequency should then be multiplied by 10,000 to get the number of ticks per second.
The alternative, which we'll adopt in our code example, is to work with the LARGE_INTEGER directly. This involves assigning the values of the LARGE_INTEGER to a Double using the following steps:
- Multiply the
highpartmember ofLARGE_INTEGERby2 ^ 32. This has the effect of shiftinghighpartto the left by 32 bits. - Add the lower 31 bits of
lowpartto theDoublevariable (or, to put it another way, mask out the sign bit fromlowpart). You can do this byANDinglowpartwith&H7FFFFFFF, which is the hexadecimal representation of a 32-bit integer with its sign bit off. - If the sign bit of
lowpartis present, add&H80000000, or 231, to theDoublevariable. (&H80000000, or 23, is the value of an unsigned long integer with only its uppermost bit set on.)
The following ConvertLargeInt function handles this conversion of a LARGE_INTEGER to a Double:
Public Function ConvertLargeInt(li As LARGE_INTEGER) As Double
Dim dbl As Double
dbl = li.highpart * 2 ^ 32 ' handle highpart
dbl = dbl + (li.lowpart And &H7FFFFFFF) ' handle all but sign bit of lowpart
If li.lowpart And &H80000000 Then dbl = dbl + 2 ^ 31 ' handle sign bit
ConvertLargeInt = dbl
End Function
Once we know the timer frequency, we can actually time code execution by calling the QueryPerformanceCounter function. Its syntax is:
Public Declare Function QueryPerformanceCounter Lib "kernel32.dll" _
(ByRef lpPerformanceCount As LARGE_INTEGER) As Boolean
Like the QueryPerformanceFrequency function, QueryPerformanceCounter has a single LARGE_INTEGER argument that, when the function returns, contains the current value of the counter. To determine elapsed time, we need to call the function when we want to start measuring code execution, then call it again when we have finished measuring code execution. Dividing the difference between the start and end times by the frequency should yield the number of seconds that the code has executed.
We can now rewrite our code example a third time to use a performance counter when comparing the time required for a For and a For Each loop to iterate an array of strings. The code is as follows:
Dim dblFreq As Double
Dim strArr(30000) As String
Private Sub Form_Load()
Dim freq As LARGE_INTEGER
Dim ctr As Integer
If QueryPerformanceFrequency(freq) Then
dblFreq = ConvertLargeInt(freq)
End If
QueryPerfFreq curFreq
For ctr = LBound(strArr) To UBound(strArr)
strArr(ctr) = "This is a short string."
Next
End Sub
Private Sub cmdFor_Click()
Dim liStart As LARGE_INTEGER, liEnd As LARGE_INTEGER
Dim dblStart As Double, dblEnd As Double
Dim ctr As Integer, ctrStart As Integer, ctrEnd As Integer
Dim sValue As String
ctrStart = LBound(strArr)
ctrEnd = UBound(strArr)
If QueryPerformanceCounter(liStart) Then
For ctr = ctrStart To ctrEnd
sValue = strArr(ctr)
Next
QueryPerformanceCounter liEnd
dblStart = ConvertLargeInt(liStart)
dblEnd = ConvertLargeInt(liEnd)
txtFor.Text = Format((dblEnd - dblStart) / dblFreq, "0.0000")
End If
End Sub
Private Sub cmdForNext_Click()
Dim liStart As LARGE_INTEGER, liEnd As LARGE_INTEGER
Dim dblStart As Double, dblEnd As Double
Dim mem As Variant
Dim sValue As String
If QueryPerformanceCounter(liStart) Then
For Each mem In strArr
sValue = mem
Next
QueryPerformanceCounter liEnd
dblStart = ConvertLargeInt(liStart)
dblEnd = ConvertLargeInt(liEnd)
txtForNext.Text = Format((dblEnd - dblStart) / dblFreq, "0.0000")
End If
End Sub
The result of running this code is shown in Figure 3. Note that the performance counter has provided us with sufficient resolution to actually time the execution of the For Each loop, although in this version it appears to run slower than the For loop.

Figure 3. The result of the QueryPerformanceCounter function
Ron Petrusha is the author and coauthor of many books, including "VBScript in a Nutshell."
Return to the Windows DevCenter.

