One Thing at a (Hundred) Times

How to use Run() and GetTickCount() for some quick and dirty performance profiling. Come for the code, stay for the scolding on premature optimization.

One Thing at a (Hundred) Times

How do you know whether one piece of code performs better than another piece of logically equivalent code?  One approach is to spend six months of your life learning about algorithms and Big O notation.  But, chances are, if you are writing applications in Microsoft Access, that's overkill.  

Your bottlenecks are almost always going to be on the data access side of things.  The solution to most Access slowdowns is likely to be some combination of better indexing, returning fewer records from the backend data source, or loading records only when you need them.

Occasionally, you may have some function that you call inside a loop that you want to be as efficient as possible.  For example, it may be a complex function that runs against hundreds of thousands of records while looping through a recordset.

For instances like those, I have a simple performance profiling trick I use.  I simply create two or more functions named Thing1(), Thing2(), etc.  I then use the Access Application.Run() method to loop through each approach to see which one is fastest.  Finally, I use the GetTickCount() Windows API call to track timing with millisecond precision.

Source Code

Here is a contrived example showcasing a few different ways to modify a string:

Sub TimeThings()
    Const NumThings As Byte = 3
    Const Iterations As Long = 200000  'Start with a small number
    Dim t As Integer, i As Long, StartTicks(1 To NumThings) As Long

    For t = 1 To NumThings
        StartTicks(t) = GetTickCount  'GetTickCount() is a Windows API call
        For i = 1 To Iterations
            Debug.Assert Run("Thing" & t, "The dog jumps") = "The fox jumps"
        Next i
        Debug.Print "Thing "; t, GetTickCount - StartTicks(t); " ms elapsed"
    Next t
End Sub

Function Thing1(Optional Val)
    Dim s As String
    s = Val
    
    ' Replace "The dog jumps" with "The fox jumps"
    Mid(s, 5, 3) = "fox"
    
    Thing1 = s
End Function

Function Thing2(Optional Val)
    Dim s As String
    s = Val
    
    ' Replace "The dog jumps" with "The fox jumps"
    s = Left(s, 4) & "fox" & Right(s, 6)
    
    Thing2 = s
End Function

Function Thing3(Optional Val)
    Dim s As String
    s = Val
    
    ' Replace "The dog jumps" with "The fox jumps"
    s = Replace(s, "dog", "fox")
    
    Thing3 = s
End Function

Sample Output

Here is my output from running the above timing examples:

'--== Immediate Window ==--
TimeThings
Thing  1       7985  ms elapsed
Thing  2       8031  ms elapsed
Thing  3       8219  ms elapsed

TimeThings
Thing  1       7953  ms elapsed
Thing  2       8000  ms elapsed
Thing  3       8234  ms elapsed

I needed to use a very large number of iterations (200,000) to find a meaningful difference between my three (admittedly simple) functions.  You should always start with a very low number of iterations and adjust it upwards as needed, otherwise you risk hanging your machine if one of the processes takes longer than you expect.

Repeatability

You should always run the TimeThings() procedure at least twice, to ensure your results are repeatable.  After all, depending on the nature of your test, there are lots of other operations outside of Access that could impact the results.  For example, if a virus scan is running during only one of the three functions could skew that function and make it appear inefficient.  If you re-run the timing suite and get similar results, it suggests that there are in fact valid performance differences between the approaches.

Premature optimization

As a final note, I should point out that programmers tend to spend too much time saving time.  Additionally, they have a tendency to favor cleverness over readability when writing code.  But look again at the three sample functions.

If we were to choose an approach based on performance alone, it would be the least readable code by far.  All three of the lines below replace "The dog jumps" with "The fox jumps".  Which one most clearly expresses the intent of the function:

Mid(s, 5, 3) = "fox"                     'Thing1: fastest

s = Left(s, 4) & "fox" & Right(s, 6)     'Thing2: middle

s = Replace(s, "dog", "fox")             'Thing3: slowest

So, sure, you can shave a few microseconds off your code with Thing1 and show off your knowledge of the obscure Mid() statement.  But if the point of the function is to replace "dog" with "fox", then I'm going with the code that basically reads like that in plain English.

Image by congerdesign from Pixabay

All original code samples by Mike Wolfe are licensed under CC BY 4.0