Making Coding Changes to Improve Performance
No matter what you do to optimize the operating system environment and improve your data design, poor code can continue to bog you down. A properly optimized application is optimized in terms of the environment, data design, and code. Just as poor table design can degrade performance, poor coding techniques also can have a dramatic negative effect on performance. Changes to your code include eliminating variants and dead code, using built-in collections, and using specific object types. An important code-related optimization is to deliver your modules precompiled.The following changes and techniques can aid in the improvement of performance. It is important to recognize that any one change won't make much of a difference. However, an accumulation of all the changes, especially where code is being re-executed in a loop, can make a significant impact on your application's performance.
Eliminate Variants and Use the Smallest Data Type Possible
Variant variables are the slowest for the operating system to process; they carry a lot of overhead because the compiler resolves them at runtime. Remember that this statement declares a variant type of variable:Dim intCounter
To strong-type this variable as an integer, for example, you must modify your code to look like this:Dim intCounter As Integer
Not only should you strong-type your variables, but you also should use the smallest data type possible. Remember that data types such as Boolean, Byte, Integer, and Long are the smallest and therefore the fastest to resolve. These are followed by Single, Double, Currency, and (finally) Variant. Of course, if you must store very large numbers with decimal points in a variable, you cannot pick Single. Just keep in mind that it is wise to select the smallest data type appropriate for the use of the variable. Listing 17.1 provides code that illustrates the difference between using a variant and a long integer.
Listing 17.1 Data Type Benchmark Illustrating the Effect of Data Type on Performance
Private Sub cmdVariantBenchMark_Click()
Dim vntAny
Dim intCounter As Long
Dim dblStartTime As Double
Dim dblTime1 As Double
Dim dblTime2 As Double
'Execute loop with variant
dblStartTime = Timer
Do Until vntAny = 500000
vntAny = vntAny + 1
Loop
dblTime1 = Timer - dblStartTime
'Execute loop with integer
dblStartTime = Timer
Do Until intCounter = 500000
intCounter = intCounter + 1
Loop
dblTime2 = Timer - dblStartTime
'Display time and percent differences
Me.txtSlow = dblTime1
Me.txtOptimized = dblTime2
Me.txtPercent = (1 - (dblTime1 / dblTime2)) * 100
End Sub
![]() | The code, found in the form frmBenchmark in the CHAP17EX.MDB sample database, loops using a variant, and then a long integer. The example displays the amount of time required to execute each loop, along with the percent difference between the two techniques. |
Use Specific Object Types
Just as using the General variant data type is inefficient, using generic object variables also is inefficient. This is because the compiler needs to evaluate their type at runtime. The MakeItBold subroutine uses a generic object variable, as shown in Listing 17.2.
Listing 17.2 The MakeItBold Subroutine
Private Sub cmdMakeBold_Click()
Call MakeItBold(Screen.PreviousControl)
End Sub
Sub MakeItBold(ctlAny As Control)
ctlAny.FontBold = True
End Sub
NOTEThe code in Listing 17.2 is overly simplified. It contains no error handling. The control passed as Screen.PreviousControl could be any type of control. The type of control received by the MakeItBold routine might not have a FontBold property, in which case an error occurs. It is therefore important for either one or both of these routines to contain proper error handling.On the other hand, the SpecificBold subroutine uses a specific object variable, as Listing 17.3 shows.
Listing 17.3 The SpecificBold Subroutine
Private Sub cmdSpecificBold_Click()
Call SpecificBold(Screen.PreviousControl)
End Sub
Sub SpecificBold(txtAny As TextBox)
txtAny.FontBold = True
End Sub
The difference is that the SpecificBold routine expects to receive only text boxes. It does not need to resolve the type of object it receives and therefore is more efficient.
![]() | This code is contained in the CHAP17EX.MDB database on the accompanying CD-ROM. You can find the example in frmObjVar. |
Listing 17.4 Object Type Benchmark Compares Using a Specific Control and a Generic Control
Private Sub cmdObjectTypes_Click()
Dim intCounter As Long
Dim dblStartTime As Double
Dim dblTime1 As Double
Dim dblTime2 As Double
'Execute loop with generic control
dblStartTime = Timer
For intCounter = 1 To 5000
Call MakeItBold(Me.txtOptimized)
Next intCounter
dblTime1 = Timer - dblStartTime
'Execute loop with specific control
dblStartTime = Timer
For intCounter = 1 To 5000
Call SpecificBold(Me.txtOptimized)
Next intCounter
dblTime2 = Timer - dblStartTime
'Display time and percent differences
Me.txtSlow = dblTime1
Me.txtOptimized = dblTime2
Me.txtPercent = (1 - (dblTime1 / dblTime2)) * 100
End Sub
![]() | The code, found in frmBenchmark, passes a text box to two different routines. The first routine receives any control as a parameter. The second routine receives only text boxes as a parameter. The benchmarks prove that routines that use specific object types take less time and are therefore more efficient. |
Use Inline Code
There is a tendency to call out to procedures for everything. This is good from a maintenance standpoint, but not from an efficiency standpoint. Each time VBA calls out to a procedure, it takes additional time to locate and execute the procedure. This is particularly evident when the procedure is called numerous times. The alternative is to use inline code. Executing inline code is more efficient than calling out to procedures, because Access does not need to locate the code. The downside of inline code is that it is more difficult to maintain. You must decide how important maintainability is compared to speed.Listing 17.5 shows the same code called as a routine and executed inline. The benchmark shows that the inline code executes much more quickly.
Listing 17.5 Inline Code Benchmark
Private Sub cmdInLine_Click()
Dim dblAny As Double
Dim intCounter As Long
Dim dblStartTime As Double
Dim dblTime1 As Double
Dim dblTime2 As Double
'Execute loop calling out to procedure
dblStartTime = Timer
For intCounter = 1 To 50000
Call SmallRoutine
Next intCounter
dblTime1 = Timer - dblStartTime
'Execute loop with inline code
dblStartTime = Timer
For intCounter = 1 To 50000
dblAny = 5 / 3
Next intCounter
dblTime2 = Timer - dblStartTime
'Display time and percent differences
Me.txtSlow = dblTime1
Me.txtOptimized = dblTime2
Me.txtPercent = (1 - (dblTime1 / dblTime2)) * 100
End Sub
Private Sub SmallRoutine()
Dim dblAny As Double
dblAny = 5 / 3
End Sub
Toggle Booleans Using Not
This code is very inefficient:If bFlag = True Then
bFlag = False
Else
bFlag = True
End If
You should modify it to look like this:bFlag = Not bFlag
![]() | Besides requiring fewer lines of code, this expression evaluates much more quickly at runtime. Listing 17.6 proves that toggling the Boolean variable is a much more efficient approach to the problem than having to test each condition separately. You can find this code in frmBenchmarks on the CD-ROM accompanying this book. |
Listing 17.6 Toggling Boolean Benchmark
Private Sub cmdBooleans_Click()
Dim boolAny As Boolean
Dim intCounter As Long
Dim dblStartTime As Double
Dim dblTime1 As Double
Dim dblTime2 As Double
'Execute loop with If statement
dblStartTime = Timer
For intCounter = 1 To 100000
If boolAny = True Then
boolAny = False
Else
boolAny = True
End If
Next intCounter
dblTime1 = Timer - dblStartTime
'Execute loop toggling Boolean
dblStartTime = Timer
For intCounter = 1 To 100000
boolAny = Not boolAny
Next intCounter
dblTime2 = Timer - dblStartTime
'Display time and percent differences
Me.txtSlow = dblTime1
Me.txtOptimized = dblTime2
Me.txtPercent = (1 - (dblTime1 / dblTime2)) * 100
End Sub
Use the Built-In Collections
The built-in collections are there whether or not you use them. By using For Each...Next and a collection of objects, you can write very efficient code, as shown in Listing 17.7.
Listing 17.7 Using For Each...Next
Sub FormCaption()
Dim frm As Form
For Each frm In Forms
frm.Caption = frm.Caption & " - " & CurrentUser()
Next
End Sub
In this example, you use the Forms collection to quickly and efficiently loop through each form, changing the caption on its title bar. The code shown in Listing 17.8 illustrates the use of the Forms collection, as well as an alternative method of accomplishing the same task.
Listing 17.8 For Each...Next Benchmark
Private Sub cmdCollections_Click()
Dim frm As Form
Dim intNumForms As Integer
Dim intLoop As Integer
Dim intCounter As Long
Dim dblStartTime As Double
Dim dblTime1 As Double
Dim dblTime2 As Double
'Execute loop with For Next
dblStartTime = Timer
For intCounter = 1 To 50
intNumForms = Forms.Count - 1
For intLoop = 0 To intNumForms
Forms(intLoop).Caption = "Hello"
Next intLoop
Next intCounter
dblTime1 = Timer - dblStartTime
'Execute loop with For Each
dblStartTime = Timer
For intCounter = 1 To 50
For Each frm In Forms
frm.Caption = "Hello"
Next frm
Next intCounter
dblTime2 = Timer - dblStartTime
'Display time and percent differences
Me.txtSlow = dblTime1
Me.txtOptimized = dblTime2
Me.txtPercent = (1 - (dblTime1 / dblTime2)) * 100
End Sub
Without the For Each..Next loop, you must use a variable to loop through the forms. Notice that the code sets intNumForms equal to the number of forms in the Forms collection minus one. The loop goes from zero to the value stored in intNumForms, changing the caption of the specified form. Although the performance gains realized by using the Forms collection are not dramatic, the Forms collection technique is much simpler to implement.
Use the Len Function
Using the Len function (as shown in Listing 17.9) is more efficient than testing for a zero-length string (as shown in Listing 17.10).
Listing 17.9 Using the Len Function
Sub SayNameLen(strName As String)
If Len(strName) Then
MsgBox strName
End If
End Sub
Listing 17.10 Testing for a Zero-Length String
Sub SayNameZero(strName As String)
If strName <> " Then
MsgBox strName
End If
End Sub
Listing 17.9 is easier for VBA to evaluate and therefore runs more quickly and efficiently. This is emphasized by the code shown in Listing 17.11 (located in frmBenchmark). The code shows two loops. One uses the Len function and the other does not. The benchmark proves that the routine that uses the Len function executes more quickly.
Listing 17.11 Len Benchmark
Private Sub cmdLen_Click()
Dim dblStartTime As Double
Dim dblTime1 As Double
Dim dblTime2 As Double
Dim strTextBoxValue As String
strTextBoxValue = Me.txtOptimized
'Execute loop with zero-length string
dblStartTime = Timer
For intCounter = 1 To 50000
If strTextBoxValue <> " Then
End If
Next intCounter
dblTime1 = Timer - dblStartTime
'Execute loop with Len
dblStartTime = Timer
For intCounter = 1 To 50000
If Len(strTextBoxValue) Then
End If
Next intCounter
dblTime2 = Timer - dblStartTime
'Display time and percent differences
Me.txtSlow = dblTime1
Me.txtOptimized = dblTime2
Me.txtPercent = (1 - (dblTime1 / dblTime2)) * 100
End Sub
Use True and False Instead of 0
This example is similar to the preceding one. It is better to evaluate for True and False (as shown in Listing 17.12) instead of 0 (as shown in Listing 17.13).
Listing 17.12 Evaluating for True and False
Sub SaySalaryTrue(lngSalary As Long)
If lngSalary Then
MsgBox "Salary is " & lngSalary
End If
End Sub
Listing 17.13 Evaluating for 0
Sub SaySalaryZero(lngSalary As Long)
If lngSalary <> 0 Then
MsgBox "Salary is " & lngSalary
End If
End Sub
The code in Listing 17.12 runs more efficiently. The benchmark shown in Listing 17.14 provides an example. The lngSalary variable is evaluated against zero in the top loop. The bottom loop tests lngSalary against True. The second loop runs more quickly.
Listing 17.14 True/False Benchmark
Private Sub cmdTrueFalse_Click()
Dim intCounter As Long
Dim lngSalary As Long
Dim dblStartTime As Double
Dim dblTime1 As Double
Dim dblTime2 As Double
'Execute loop with zero
dblStartTime = Timer
For intCounter = 1 To 50000
If lngSalary <> 0 Then
End If
Next intCounter
dblTime1 = Timer - dblStartTime
'Execute loop with True/False
dblStartTime = Timer
For intCounter = 1 To 50000
If lngSalary Then
End If
Next intCounter
dblTime2 = Timer - dblStartTime
'Display time and percent differences
Me.txtSlow = dblTime1
Me.txtOptimized = dblTime2
Me.txtPercent = (1 - (dblTime1 / dblTime2)) * 100
End Sub
NOTEAlthough both the Len function and the use of True/False provided consistently better results in Access 2000 and earlier versions of Access, they appear to provide inconsistent results in Access 2003. This leads me to believe that there might have been changes to how these functions or their alternatives execute in Access 2003. I suggest that you test both the Len function and the use of True/False in your own environments to determine whether they result in improved performance.
Eliminate Unused Dim and Declare Statements
As you modify your subroutines and functions, you often declare a variable and then never use it. Each Dim statement takes up memory, whether or not you are using it. Furthermore, Declare statements, which you use to call external library functions, also take up memory and resources. You should remove these statements if you are not using them.
Eliminate Unused Code
Most programmers experiment with various alternatives for accomplishing a task. This often involves creating numerous test subroutines and functions. The problem is that most people do not remove this code when they are done with it. This dead code is loaded with your application and therefore takes up memory and resources. Several third-party tools are available that can help you find both dead code and variable declarations. One tool that many people use is called Total Access Analyzer, by FMS, Inc. (http://www.fmsinc.com).
Use Variables to Refer to Properties, Controls, and Data Access Objects
If you are going to repeatedly refer to an object, you should declare an object and refer to the object variable rather than the actual control, as shown in Listing 17.15.
Listing 17.15 Declaring an Object and Referring to the Object Variable
Forms!frmAny.txtHello.FontBold = True
Forms!frmAny.txtHello.Enabled = True
Forms!frmAny.txtHello.Left = 1
Forms!frmAny.txtHello.Top = 1
This is a very scaled-down example, but if numerous properties are being changed, or if this code is being called recursively, an object variable can make the code more efficient, as Listing 17.16 shows.
Listing 17.16 Using an Object Variable to Make Your Code More Efficient
Private Sub cmdChangeObject_Click()
Dim txt As TextBox
Set txt = Forms!frmHello.txtHello1
txt.FontBold = True
txt.Enabled = True
txt.Left = 100
txt.Top = 100
End Sub
The benchmark shown in Listing 17.17 contains two loops. The first loop sets four properties of the same control, explicitly referencing the control as each property is set. The second loop uses an object variable to accomplish the same task. The difference in performance between the two loops is somewhat dramatic.
Listing 17.17 Object Variable Benchmark
Private Sub cmdObjectVariable_Click()
Dim intCounter As Long
Dim dblStartTime As Double
Dim dblTime1 As Double
Dim dblTime2 As Double
'Execute loop without object variable
dblStartTime = Timer
For intCounter = 1 To 1000
Forms.frmBenchMark.txtOptimized.FontBold = True
Forms.frmBenchMark.txtOptimized.Enabled = True
Forms.frmBenchMark.txtOptimized.Locked = False
Forms.frmBenchMark.txtOptimized.BackStyle = vbNormal
Next intCounter
dblTime1 = Timer - dblStartTime
'Execute loop with object variable
dblStartTime = Timer
For intCounter = 1 To 1000
Dim txt As TextBox
Set txt = Forms.frmBenchMark.txtOptimized
txt.FontBold = True
txt.Enabled = True
txt.Locked = False
txt.BackStyle = vbNormal
Next intCounter
dblTime2 = Timer - dblStartTime
'Display time and percent differences
Me.txtSlow = dblTime1
Me.txtOptimized = dblTime2
Me.txtPercent = (1 - (dblTime1 / dblTime2)) * 100
End Sub
Use With...End With
Another way to optimize the code in the preceding example is to use a With...End With construct, as shown in Listing 17.18.
Listing 17.18 Using With...End With
Private Sub cmdChangeObjectWith_Click()
With Forms!frmHello.txtHello2
.FontBold = True
.Enabled = True
.Left = 100
.Top = 100
End With
End Sub
The code in Listing 17.19 shows two different loops. The first loop explicitly references the text box four different times to set four different properties. The second loop uses a With statement to reference the same control and set the four properties. The code in the second loop executes much more efficiently.
Listing 17.19 Object Variable Resolution Benchmark
Private Sub cmdWith_Click()
Dim intCounter As Long
Dim dblStartTime As Double
Dim dblTime1 As Double
Dim dblTime2 As Double
'Execute loop without With statement
dblStartTime = Timer
For intCounter = 1 To 1000
Forms.frmBenchMark.txtOptimized.FontBold = True
Forms.frmBenchMark.txtOptimized.Enabled = True
Forms.frmBenchMark.txtOptimized.Locked = False
Forms.frmBenchMark.txtOptimized.BackStyle = vbNormal
Next intCounter
dblTime1 = Timer - dblStartTime
'Execute loop with With statement
dblStartTime = Timer
For intCounter = 1 To 1000
With Forms.frmBenchMark.txtOptimized
.FontBold = True
.Enabled = True
.Locked = False
.BackStyle = vbNormal
End With
Next intCounter
dblTime2 = Timer - dblStartTime
'Display time and percent differences
Me.txtSlow = dblTime1
Me.txtOptimized = dblTime2
Me.txtPercent = (1 - (dblTime1 / dblTime2)) * 100
End Sub
Resolve Variables Outside a Loop
Although both the object variable reference and the With statement significantly improve performance, Listings 17.17 and 17.19 can be further improved by resolving the object variable outside the loop whenever possible. Listing 17.20 provides an example.
Listing 17.20 Resolving the Object Variable Outside the Loop
Private Sub cmdVariable_Click()
Dim txtAny As TextBox
Dim intCounter As Long
Dim dblStartTime As Double
Dim dblTime1 As Double
Dim dblTime2 As Double
'Execute loop without object resolution
dblStartTime = Timer
For intCounter = 1 To 1000
Forms.frmBenchmark.txtOptimized.FontBold = True
Forms.frmBenchmark.txtOptimized.Enabled = True
Forms.frmBenchmark.txtOptimized.Locked = False
Forms.frmBenchmark.txtOptimized.BackStyle = vbNormal
Next intCounter
dblTime1 = Timer - dblStartTime
'Execute loop with object resolution
dblStartTime = Timer
Set txtAny = Forms.frmBenchmark.txtOptimized
For intCounter = 1 To 1000
With txtAny
.FontBold = True
.Enabled = True
.Locked = False
.BackStyle = vbNormal
End With
Next intCounter
dblTime2 = Timer - dblStartTime
'Display time and percent differences
Me.txtSlow = dblTime1
Me.txtOptimized = dblTime2
Me.txtPercent = (1 - (dblTime1 / dblTime2)) * 100
End Sub
Notice that the object variable is resolved outside the loop. This loop executes significantly faster than the loops in Listings 17.17 and 17.19.
Use the Me Keyword
The preceding example uses Forms!frmHello.txtHello to refer to a control on the current form. It is more efficient to refer to the control as Me.txtHello because VBA searches only in the local name space. Although this makes your code more efficient, the downside is that the Me keyword only works within form, report, and class modules. It won't work within standard code modules. This means that you cannot include the Me keyword in generic functions that are accessed by all your forms.
Use String Functions When VBA Provides a Variant and String Alternative
Several VBA functions come in two forms :
one with a dollar sign ($) and one without. An example is Left(sName) versus Left$(sName). Whenever it is acceptable to return a string, it is more efficient to use the version with the dollar sign, which return strings rather than variants. When a string variable is returned, VBA doesn't need to perform type conversions.
Use Dynamic Arrays
Array elements take up memory, whether or not you use them. It's therefore sometimes preferable to use dynamic arrays. You can increase the size of a dynamic array as necessary. If you want to reclaim the space used by all the elements of the array, you can use the Erase keyword, as in this example:Erase aNames
If you want to reclaim some of the space being used by the array without destroying data in the elements you want to retain, use Redim Preserve:Redim Preserve aNames(5)
This statement sizes the array to six elements (it's zero-based). Data within those six elements is retained.CAUTIONYou must be careful when using dynamic arrays with Redim Preserve. When you resize an array using Redim Preserve, the entire array is copied in memory. If you are running in a low-memory environment, this can mean that virtual disk space is used, which slows performanceor worse than that, the application can fail if both physical and virtual memory are exhausted.
Use Constants When They Are Available
Constants improve both readability and performance. A constant's value is resolved after compilation. The value the constant represents is written to code. A normal variable has to be resolved as the code is running because VBA needs to obtain the current value of the variable.
Use Bookmarks
A bookmark provides you with the most rapid access to a record. If you are planning to return to a record, set a variable equal to that record's bookmark, making it very easy to return to that record at any time. Listing 17.21 shows an example that uses a bookmark.
Listing 17.21 Using a Bookmark
Sub BookMarkIt()
Dim rst As ADODB.Recordset
Set rst = New ADODB.Recordset
Dim varBM As Variant
rst.Open "tblProjects", CurrentProject.Connection, adOpenStatic
varBM = rst.Bookmark
Do Until rst.EOF
Debug.Print rst!ProjectID
rst.MoveNext
Loop
rst.Bookmark = varBM
Debug.Print rst!ProjectID
End Sub
Set Object Variables Equal to Nothing
Object variables take up memory and associated resources. Their value should be set to Nothing when you are finished using them. For example:Set oObj = Nothing
This conserves memory and resources.
Use Action Queries Instead of Looping Through Recordsets
Besides being easier to code, executing a stored query is much more efficient than looping through a recordset, performing some action on each record. Listing 17.22 shows an example that loops through a recordset.
Listing 17.22 Looping Through a Recordset
Sub LoopThrough()
Dim rst As ADODB.Recordset
Set rst = New ADODB.Recordset
rst.Open "tblProjects", CurrentProject.Connection, adOpenDynamic, _
adLockOptimistic
Do Until rst.EOF
rst!ProjectTotalEstimate = rst!ProjectTotalEstimate + 1
rst.UPDATE
rst.MoveNext
Loop
End Sub
![]() | This code, which is located in basOptimize of CHAP17EX.MDB, loops through a recordset, adding 1 to each project's total estimate. Contrast this with the code in Listing 17.23. |
Listing 17.23 Executing a Stored Query
Sub ExecuteQuery()
Dim adoCat As ADOX.Catalog
Dim cmd As ADODB.Command
Set adoCat = New ADOX.Catalog
Set cmd = New ADODB.Command
Set adoCat.ActiveConnection = CurrentProject.Connection
Set cmd = adoCat.Procedures("qupdLowerEstimate").Command
cmd.Execute
End Sub
This code uses a command object to execute a stored query called qupdLowerEstimate. The query runs much more efficiently than the Do...Until loop shown in Listing 17.22.NOTEThe most efficient method to update records is with a stored procedure. You can use stored procedures with a client/server database engine such as Microsoft SQL Server. This is covered in detail in Alison Balter's Mastering Access 2002 Enterprise Development .
Deliver Your Application with the Modules Compiled
Applications run slower when they are not compiled. Forms and reports load slower, and the application requires more memory. If you deliver your application with all the modules compiled, they do not need to be compiled on the user's machine before they run.To easily recompile all modules, choose Debug, Compile with the Visual Basic Editor (VBE) active. This command opens and compiles all code in the application, including the code behind forms and reports. It then saves the modules in the compiled state, preserving the compiled state of the application.
Retain the Compiled State
Don't bother choosing the Debug, Compile command if you plan to make additional changes to the application. An application becomes decompiled whenever you modify the application's controls, forms, reports, or modules. Even something as simple as changing a single line of code causes the application to lose its compiled state. It's therefore important to choose the Debug, Compile command immediately before you distribute the application.CAUTIONRenaming a database file causes the code contained in the database to decompile. It's therefore important to always choose the Compile command after renaming a database.
Distribute Your Application as an MDE
The process of creating an MDE file compiles all modules, removes editable source code, and compacts the destination database. All Visual Basic code will run, but cannot be viewed or edited. This improves performance, reduces the size of the database, and protects your intellectual property. Memory use also is improved. The process of saving an application as an MDE and the implications of doing so are covered in Chapter 27, "Database Security Made Easy," and Chapter 32, "Distributing Your Application."
Organize Your Modules
VBA code theoretically can be placed in any module within your application. The problem is that the compiler does not load a module until you call a function within it. After you call a single procedure in a module, the compiler loads the entire module into memory. Furthermore, if a single variable within a module is used, the compiler loads the entire module into memory. As you might imagine, if you design your application without much thought, every module in your application will be loaded.If you place similar routines all in one module, that module will be loaded, and others will not. This means that if people are using only part of the functionality of your application, they will never be loading other code modules. This conserves memory and therefore optimizes your application.