VBA Efficiency

Efficiency is concerned with eliminating waste and thereby improving the speed and performance of code. There are a number of practices that help to improve the efficiency of VBA code.

Close the Visual Basic Editor

The Visual Basic Editor has a status bar which updates continuously while VBA code is running which can severely reduce speed and performance. When running VBA code that does a lot of processing and iterating, closing the Visual Basic Editor drastically improves performance and speed. In some cases, programs can become unresponsive if the Visual Basic Editor is open.

Avoid Variants

Variants require at least 16 bytes of memory which is more than other types require. Using appropriate data types can save space and make programs run faster. Especially avoid large arrays of Variants if possible.

Public Sub TimeVariantVsLong()

    Dim i      As Long
    Dim j      As Long
    Dim t      As Single
    Dim VArr() As Variant
    Dim LArr() As Long

    Const N As Long = 1000

    'Variant
    t = Timer
    For i = 1 To N
        ReDim VArr(0 To 9999) As Variant
        For j = LBound(VArr) To UBound(VArr)
            VArr(j) = j
        Next j
    Next i
    t = Timer - t
    Debug.Print "Variant: " & t

    'Long
    t = Timer
    For i = 1 To N
        ReDim LArr(0 To 9999) As Long
        For j = LBound(LArr) To UBound(LArr)
            LArr(j) = j
        Next j
    Next i
    t = Timer - t
    Debug.Print "Long: " & t

End Sub

Suppress External Activity

Running a VBA program can cause external activity which can slow down the program. It may improve efficiency to disable one or more of the below settings to avoid unnecessary external activity. Just be sure the program does not rely on any of the settings.

Public Sub Example()

    Application.ScreenUpdating = False
    Application.EnableEvents = False
    Application.Calculation = xlCalculationManual

    'Code Here

    Application.Calculation = xlCalculationAutomatic
    Application.EnableEvents = True
    Application.ScreenUpdating = True

End Sub

Screen Updating

While a VBA program is running, the application will continuously update the screen to capture the changes made by the running program. This is often unnecessary and inefficient. The screen normally only needs to be updated after the program has finished running to reflect all changes. To prevent the screen from continuously updating while the program is running set the Application.ScreenUpdating property to False.

Events

Events can be triggered by a program while it is running. Often times, it is not necessary or desirable to allow a running program to trigger events. Most often, events are meant to be triggered by user interactions only. If events do not need to be triggered by the program, set the Application.EnableEvents property to False to prevent events from being triggered.

Calculation

If an Excel spreadsheet contains volatile functions these functions will recalculate whenever a change is made anywhere in Excel. This may not be necessary and can slow down a program. If there is no need to recalculate after every change then set the Application.Calculation property to xlCalculationManual while the code is running. To revert back to the default calculation method set the property to xlCalculationAutomatic.

There are built-in volatile functions in Excel and user-defined functions can be made volatile. To make a user-defined function volatile, use the Application.Volatile method.

Built-In Volatile Functions:

  • NOW
  • TODAY
  • RANDBETWEEN
  • OFFSET
  • INDIRECT
  • INFO
  • CELL
  • SUMIF

Avoid String Reallocation

Reallocating strings can be very inefficient and unnecessary. Specialized string classes can be used to reduce string reallocation while providing a high level of functionality. The Mid function can also be used to avoid reallocating a string by assigning parts of a string instead of reallocating an entirely new string.

String Classes

To avoid unnecessary string reallocations, use the clsStringBuilder and clsArrayListString classes.

Public Sub TimeclsStringBuilder()

    Dim i As Long
    Dim j As Long
    Dim t As Single
    Dim S As String
    Dim SB As clsStringBuilder

    Const N As Long = 100000

    'Concatenation
    t = Timer
    For i = 1 To N
        S = S & "A"
    Next i
    t = Timer - t
    Debug.Print "Concatenation: " & t

    'clsStringBuilder
    Set SB = New clsStringBuilder
    t = Timer
    For i = 1 To N
        SB.Append "A"
    Next i
    t = Timer - t
    Debug.Print "clsStringBuilder: " & t

End Sub
Public Sub TimeclsArrayListString()

    Dim i As Long
    Dim j As Long
    Dim t As Single
    Dim S As String
    Dim AL As clsArrayListString

    Const N = 100000

    'Concatenation
    t = Timer
    For i = 1 To N
        S = S & "A" & vbNewLine
    Next i
    S = Left$(S, Len(S) - Len(vbNewLine))
    t = Timer - t
    Debug.Print "Concatenation: " & t

    'clsArrayListString
    Set AL = New clsArrayListString
    t = Timer
    For i = 1 To N
        AL.Append "A"
    Next i
    S = AL.JoinString(vbNewLine)
    t = Timer - t
    Debug.Print "clsStringBuilder: " & t

End Sub

Mid Function

A technique to make working with strings more efficient is to allocate a larger string than is needed and use the Mid function to change parts of the string.

Public Sub TimeMid()

    Dim i As Long
    Dim j As Long
    Dim t As Single
    Dim S1 As String
    Dim S2 As String

    Const N As Long = 100000

    'Concatenation
    t = Timer
    For i = 1 To N
        S1 = S1 & "A"
    Next i
    t = Timer - t
    Debug.Print "Concatenation: " & t

    'Mid
    t = Timer
    S2 = String$(N, " ")
    For i = 1 To N
        Mid$(S2, i, 1) = "A"
    Next i
    t = Timer - t
    Debug.Print "Mid: " & t

End Sub

Use For Each Loops with Collections

For Each loops are more efficient for looping over Collections because accessing Collection items using the Item method is inefficient. Because For Each loops access each item in the Collection via the control variable, there is no need to use the Item method when using a For Each loop.

Public Sub TimeForEachVsForWithCollection()

    Dim i As Long
    Dim j As Long
    Dim k As Long
    Dim v As Variant
    Dim C As Collection
    Dim t As Single

    Const N As Long = 10000

    Set C = New Collection
    For i = 1 To 100
        C.Add i
    Next i

    'For
    t = Timer
    For i = 1 To N
        For j = 1 To C.Count
            k = C(j)
        Next j
    Next i
    t = Timer - t
    Debug.Print "For: " & t

    'For Each
    t = Timer
    For i = 1 To N
        For Each v In C
            k = v
        Next v
    Next i
    t = Timer - t
    Debug.Print "For Each : " & t

End Sub

Use Arrays Over Ranges

When doing operations on Excel spreadsheet data it can be more efficient to copy the values from a Range into an array, do the operations on the array values, and then copy the array back to the Range.

Public Sub TimeArrayVsRange()

    Dim i As Long
    Dim j As Long
    Dim t As Single
    Dim Arr As Variant

    Const N As Long = 1000

    Application.ScreenUpdating = False

    'Range
    t = Timer
    For i = 1 To N
        Range("A1:A10") = [{1;2;3;4;5;6;7;8;9;10}]
        For j = 1 To 10
            Cells(j, 1).Value = _
            Cells(j, 1).Value * Cells(j, 1).Value
        Next j
    Next i
    t = Timer - t
    Debug.Print t

    'Array
    t = Timer
    For i = 1 To N
        Range("A1:A10") = [{1;2;3;4;5;6;7;8;9;10}]
        Arr = Range("A1:A10").Value
        For j = 1 To 10
            Arr(j, 1) = Arr(j, 1) * Arr(j, 1)
        Next j
        Range("A1:A10") = Arr
    Next i
    t = Timer - t
    Debug.Print t

    Application.ScreenUpdating = True

End Sub

Avoid Selecting

When working with Ranges in Excel VBA it is usually unnecessary to select a Range. Selecting Ranges is inefficient and can slow down a VBA program. It is more efficient to refer directly to a Range using the Range property or to use the Cells property to retrieve the Range than to select a Range using the Select method and then access the Range through the Selection property.

Public Sub TimeSelectVsRangeOrCells()

    Dim i As Long
    Dim t As Single

    Const N As Long = 1000

    Application.ScreenUpdating = False

    'Select
    t = Timer
    Range("A1").Select
    For i = 1 To N
        Selection.Value = i
        Selection.Offset(1, 0).Select
    Next i
    t = Timer - t
    Debug.Print "Select: " & t

    'Cells
    t = Timer
    For i = 1 To N
        Cells(i, 1).Value = i
    Next i
    t = Timer - t
    Debug.Print "Cells: " & t

    Application.ScreenUpdating = True

End Sub

Objects

Allocating and deallocating objects can be inefficient. To improve efficiency, avoid creating objects inside procedures that will be called frequently. In the example below it might appear that it is more efficient to look up a value from a Dictionary because elements of a Dictionary are searched in constant time (O(1)) whereas elements of an array are searched in linear time (O(n)). However, because of the overhead of allocating and deallocating the Dictionary object for each function call, the less efficient array lookup is actually more efficient for small list sizes.

Option Explicit

Public Sub TimeDictionaryVsArrayForSmallList()

    Const N As Long = 100

    Dim i As Long
    Dim t As Single
    
    'Dictionary (more efficient search)
    t = Timer
    For i = 1 To N
        LookupValueFromDict "D"
    Next i
    t = Timer - t
    Debug.Print "Dictionary: " & t

    'Array (less efficient search)
    t = Timer
    For i = 1 To N
        LookupValueFromArray "D"
    Next i
    t = Timer - t
    Debug.Print "Array: " & t

End Sub

Public Function LookupValueFromDict(Key As String) As Variant
    
    'Allocating and deallocating a Dictionary object
    'with each function call hurts performance

    Dim Dict As Object
    Set Dict = CreateObject("Scripting.Dictionary")

    Dict.Add "A", 1
    Dict.Add "B", 2
    Dict.Add "C", 3
    Dict.Add "D", 4

    If Dict.Exists(Key) Then
        LookupValueFromDict = Dict(Key)
    End If

End Function

Public Function LookupValueFromArray(Key As String) As Variant
    
    Dim KeyArr(0 To 3) As String
    KeyArr(0) = "A"
    KeyArr(1) = "B"
    KeyArr(2) = "C"
    KeyArr(3) = "D"

    Dim ValueArr(0 To 3) As Variant
    ValueArr(0) = 1
    ValueArr(1) = 2
    ValueArr(2) = 3
    ValueArr(3) = 4

    Dim i As Long
    For i = LBound(KeyArr) To UBound(KeyArr)
        If KeyArr(i) = Key Then
            LookupValueFromArray = ValueArr(i)
            Exit Function
        End If
    Next i

End Function

User-Defined Types Vs Classes

It is more efficient to use an array with a user-defined type than to use an array or Collection with a class. Classes need to be instantiated whereas user-defined types do not. Allocating and deallocating objects is costly and should be avoided where possible. In the example below, because there are no methods for the class, it is better to use a user-defined type instead.

'Class Module: clsExample

Option Explicit

Public Message As String
Public Flag    As Boolean
'Standard Module: Module1

Option Explicit

Public Type TExample
    Message As String
    Flag    As Boolean
End Type

Public Sub TimeUserDefinedTypeVsClass()

    Const N As Long = 10000

    Dim i As Long
    Dim t As Single

    Dim TEArr() As TExample
    Dim TE As TExample

    Dim CEArr() As clsExample
    Dim CEColl As Collection
    Dim CE As clsExample

    'User-Defined Type and Array
    t = Timer
    ReDim TEArr(1 To N) As TExample
    For i = 1 To N
        TEArr(i).Message = "Hello, World!"
        TEArr(i).Flag = True
    Next i
    t = Timer - t
    Debug.Print "User-Defined Type and Array: " & t

    'Class and Array
    t = Timer
    ReDim CEArr(1 To N) As clsExample
    For i = 1 To N
        Set CEArr(i) = New clsExample
        CEArr(i).Message = "Hello, World!"
        CEArr(i).Flag = True
    Next i
    t = Timer - t
    Debug.Print "Class and Array: " & t

    'Class and Collection
    t = Timer
    Set CEColl = New Collection
    For i = 1 To N
        Set CE = New clsExample
        CE.Message = "Hello, World!"
        CE.Flag = True
        CEColl.Add CE
    Next i
    t = Timer - t
    Debug.Print "Class and Collection: " & t

End Sub

Use DLLs

DLLs can be created using faster lower level languages like C++ and C# which can be used in VBA. Using a DLL function may carry its own overhead so may not be suitable for relatively small tasks. In the example below, calling a VBA function outperforms calling a C++ DLL function for small array sizes but calling the C++ DLL function outperforms calling the VBA function for larger array sizes. DLL functions can also be multi-threaded whereas VBA functions are single-threaded. Multi-threading can drastically improve speed.

// C++ ExcelFunctions.dll

#define DLLExport __declspec(dllexport)

extern "C" {

    DLLExport int __stdcall CppSum(int values[], int arraySize) {

        int theSum = 0;

        for (int i = 0; i < arraySize; ++i) {
            theSum += values[i];
        }

        return theSum;
    }
    
}
Option Explicit

Private Declare PtrSafe Function CppSum Lib "C:\ExcelFunctions.dll" ( _
ByRef values As Long, ByVal arraySize As Long) As Long

Private Function VBASum(values() As Long, arraySize As Long) As Long
    Dim i As Long
    For i = 0 To arraySize - 1
        VBASum = VBASum + values(i)
    Next i
End Function

Public Sub TimeDLLVsVBA()

    Dim Result As Long
    Dim i      As Long
    Dim t      As Single
    Dim Arr()  As Long

    Const N As Long = 10000

    Const ARRAY_SIZE As Long = 100
    'C++: 0.078125; Result: 5050
    'VBA: 0.0078125; Result: 5050

    'Const ARRAY_SIZE As Long = 1000
    'C++: 0.109375; Result: 500500
    'VBA: 0.109375; Result: 500500

    'Const ARRAY_SIZE As Long = 10000
    'C++: 0.0859375; Result: 50005000
    'VBA: 1.328125; Result: 50005000

    ReDim Arr(0 To ARRAY_SIZE - 1) As Long
    For i = LBound(Arr) To UBound(Arr)
        Arr(i) = i + 1
    Next i

    'C++
    t = Timer
    For i = 1 To N
        Result = CppSum(Arr(LBound(Arr)), ARRAY_SIZE)
    Next i
    t = Timer - t
    Debug.Print "C++: " & t & "; Result: " & Result

    'VBA
    t = Timer
    For i = 1 To N
        Result = VBASum(Arr, ARRAY_SIZE)
    Next i
    t = Timer - t
    Debug.Print "VBA: " & t; "; Result: " & Result

End Sub