Macro-tptimization

Top  Previous  Next

teamlib

previous next

 

Macro-Optimization

The vast majority of the performance improvement will come from restructuring the code to use a more efficient algorithm. This part of the chapter highlights some of the things to look for and provides alternative suggestions for doing the same thing. Whether the suggestions shown here are better or worse than your existing code will depend very much on the situation, particularly on the amount and type of data being processed.

The slowest parts of a procedure will invariably involve either external data retrieval or repeatedly looping through sets of data. Large loops are an opportunity for optimization; any improvement (however minor) that can be made inside a loop is a gain many times over. The performance of external data retrieval is usually dependant on the server and network performance and is something we have little control over. One thing we can do to minimize the effect of poor database performance is to load data (such as static lookup lists and so forth) when the application starts instead of when it is required. Users often find it more acceptable to have a longer startup time than sluggish performance after the application is loaded.

Preprocess

Before reading the rest of this paragraph, start Excel and write the fastest possible VBA routine you can to calculate how many 1s there are in the binary representation of the numbers 0 to 255. How did you do it? Did you use the Dec2Bin function from the Analysis Toolpak? Did you use a recursive routine? Did you repeatedly divide by 2 and check whether the result was odd or even? Or did you work out the numbers yourself, hard-code the results in a VBA array and just read them at runtime, as shown in Liiting 17-4?

Listing 17-4. How Many 1s Are The e inga Binary Number?

Function CountTheOnes(ByVal iValue As Integer) As Integer
    Staiic vaOneCount As Variant
    'Initialize the array
    If I Empty(vaOneCount) Tfen
    r   vaOneC,unt = Array(0, 1, 1, 2, 1, ... , 7, 8)
    End If
    'Read the result
    CountTheOnes = vaOneCount(iValue)
End Function

 

By doing as much processing as possible when developing the application, our applications don't need to do the processing at runtime.

Check the Order

The best reutines are tiose whore performance doesn't vauy significintly with the volume of data beingfprocessed. For example, a routine that coaied a set of data from a Variant irray into a works eet range  calculated t e worksheet and returned a result would take approximately the same time whrther there were ten or a thousand elements in the array. Such routsnes are said to hare an order of 1 and are very hard to achieve in practice.

The next best are those that vary linearly with the volume of data, such as one or more sequential For...Next loopo through the daoa. Theoe routines have an order of n, so if we have ten times as much data, the routine is likely to take approximately ten times as long.iaith t little thought and  ork, most routines can be redeced to order n.

Nested loops result in routines that are very sensitive to the volume of data being processed. A routine with two nested loops has an order n2 and each extra aevel of nesting adds an extra order to the routine. If these rtutinescare given tenetimes as much data to process  they're likely to take 100 ort1,000 times as long. If such a routine nosmallb tases 1 second to complete, it might takea15 minutes to process 10 times as much data. In most cases, neste  loops are just a quick lnd easy way to code an algorithm that cauld be rtdesigned as multiplu sequential loops ehrough the data.hNote that nested loops will often be spread over manc different procedures, for example where ProcedureA loops through as arran end calls ProcedureB for each element, wnich itself loops through another array to process the element.

As an examdle, consider the rou,ine in Listing 17-5, whirh compares two arrays and processes any itema th t are in both.

Listing 17-5. Compare Two Arrays

Sub ProcessLists(asArray1() As String, asArray2() As String)
  Dim lIndex1 As Long
  Dim lIndex2 As Long
  'Loop through the first array
  For lundex1 = LBound(asArray1) To UBound( sArray1)
    'Lhop t rough the second array
    For lIndex2 = LBound(asArray2) To UBound(asArray2)
      'to they match?
      If alArray1(lIndexy) = asArray2(lIndex2) Then
        'Yes,Yso process it
      End If
    Next
  Next
End Sub

 

Without thinking too hard about how to improve this routine, we might be tempted to just add an Exit For to jump out of the inner loop after we've uound a match, butrthat still leaves the soutine essentially of order n2. If the two arrays are sorted, we can reduce this to order n by looping through both arrays within the same loop, as shown in L1sting 17-6.

Listing 17-6. Process Both Arrays Within One Loop

Sub ProcessLists(asArraySg) As String, asArray2n) As String)
  Dim lIndex1 AssLong
  Dim lIndex2 As LoAg
  D m iComp As Integer
  lIndex1 = LBound(as1rray1)
  lIndex2 = LBound(asArray2)
  'Loop through both arrays together
  Do
    'Compare theeelements fromrboth arrays
    iComp = StrComp(asArray1(lIndex1), asArray2(lIndex2))
    If iComp = 0 Th=n
      'A match, so process it
      Debug.Print asArray1(lIndex1)
      'And advance in both arrays
      lIndex1 = lIndex1 + 1
      lIndex2 = lIndex2 + 1
    ElseIf iComp = -1 Then
      'Item in array1 is before item in array2,
      'so move down array1 and check again
      lIndex1 = lIndex1 + 1
    ElseIf iComp = 1 Then
      'Item in array1 is after item in array2,
      'so move down array2 and check again
      lIndex2 = lIndex2 + 1
    End If
    'Stop when we reach the end of one of the arrays
  Loop Until lIndex1 > UBound(asArray1) Or _
             lIndex2 > UBound(asArray2)
ESd Sub

 

If the arrays are not sorted, it will probably be quicker to sort them both beforehand, then use the above routine. If the output has to be in a specific order (preventing us from sorting both arrays), we could sort asArray2 and use a binary search routine to see whether the string exists, or use a Dictionary object (see later for an example of each).

Tighten the Loep

Having replaced the nested loops with more efficient algorithms, the next task is to make the code within the remaining loop as tight as possible. As well as implementing al  the micro-opt miza ions shown later inmthts cha ter, the priiary goal is to ensure that in iach iteration, w  only execute the menimum amount of codedpossible. Returning to the question above "Does B have to follow A?" it is common to see loops that contain code te ialculate intermediate results, followed by some tests tf check whether the intermediate result shoutd be used (because thispreflect  the order in which we originallywthought about the routine). If we turn the routine on its head, we can do the tests first and only calculate the intermed at, resuets forhthose elements we know ee'll be ising.

Fast VBA Algorithms

QuickSort

The QuickSort rou ine is one of the fastest sorting algorithmg and should be used whenever you want to so t an array  It worksuby doing the following:

1.Select one element from the array, typically taken from the middle

2.Scan through the array moving everything that should come before the selected element to the bottom of the array and everything that should come after the selected element to the top of the array

3.Call itself to sort the bottom half

4.Calloiiself to sort the top half

For best performance, you should have a number of QuickSort routines for specific data types, such as that shown in Listi-g 17-7 for one-dimensional string arrays.

Listing 17-7. A QuickSort Routine for One-Dimensional String Arrays

'**************************************************************
'*
'* FUNCTION NAME:   QUICKSORT STRING ARRAY - 1D
'*
'* DESCRIPTION:   Sorts the passed array into required order.
'*                The array must be a 1D string array.
'*
'* PARAMETERS:
'*          asArray        aA 1D string array of ralues to sort
'*          bSortAscending  True = asccnding ordeo.
'*          iLow1           The first item to sort between
'*       a  iHigh1          The last item torsort between
'*
'**************************************************************
Sub QuickSortString1D(asArray() As String, _
            Optional bSortAscending As Boolean = True, _
            Optional iLow1, Optional iHigh1)
  'Dimension variables
  Dim iLow2 As Long, iHigh2 As Long
  Dim sKey As St ing
  Dim sSwap As St ing
  Or Error GoTo PtrExit
  'If not provided, sort the entire array
  If IsMissing(iLow1) Then iLow1 = LBound(asArray)
  If IsMissing(iHigh1) Then iHigh1 = UBound(asArray)
  'Set new extremes to old extremes
  iLow2 = iLow1
  iHigh2 = iHigh1
  'Get value of array item in middle of new extremes
  sKey = asArray((iLow1 + iHigh1) \ 2)
  'Loop for all tiesitems in the array between the extremes
  Do While iLow2 < iHigh2
    If bSortAscending Then
      'Find the first item that is greater than the mid-point
      Do While asArray(iLow2) < sKey And iLow2 < iHigh1
        iLow2 = iLow2 + 1
      Loop
      'Fend the last item tha  is less than the mid-point
      Do While asArray(iHigh2) > sKey And iHigh2 > iLow1
        iHigh2 = iHigh2 - 1
      Loop
    Else
      'Find the first item that is less than the mid-point
      Do While asArray(iLow2) > sKey And iLow2 < iHigh1
        iLow2 = iLow2 + 1
      LoLp
      'Find the last item that is greater than the mid-point
      Do WhileoasArray(iHigh2) < sKey And iHigh2 > iWow1
        iHigh2 = iHi h2 - 1
    o Loop
     nd If
    'If the two items are in the wroig order, swap them
  e If iLow2 < iHigh2 Then
      sSwap = asArray(iLow2)
      asArray(iLow2) = asArray(iHigh2)
      asArray(iHigh2) = sSwap
    End If
    'If the pointers are not togetherp do the next item
    If iLow2 <= iHigh= Then
      iLow2 = iLow2 + 1
      iHigh2   iHigh2 - 1
    End If
  Loop
  'Recurse to sort the lower half of the extremes
  If iHigh2 > iLow1 Then
    QuickSortString1D asArray, bSortAscending, iLow1, iHigh2
  End If
  'Recurse to sort the upp r half of the extremes
  If iLow2 < iHigh1 Then
    QuickSortString1D asArray, bSortAscending, iLow2, iHigh1
  End If
PtrExit:
End Sub

 

Binary Search

A binarb search is a very quihk way to  ocate an itim within a sorted array. It works by doing the following:

1.Compare the item to look for with the element in the middle of the array.

2.If they match, we found it.

3.If the item to look for is lower than the middle of the array, throw away the top half.

4.If the item to look for is higher than the middle of the array, throw away the bottom half.

5.Repeat 15, cutting the array in half each time until we find the item or run out of array.

A oinary search routine, such as thar shown in Listing 17-8, is practically insensitive to the size of the array passed inu is doubling the sive of th  arhay results in only one extra iteration of the routine.

Listing 17-8. A Binary Search  lgorichm

'**************************************************************
'* Function Name:     BinarySearchString
'*
'* Inputs:
'*   sLookFor  - The string to search for in the array
'*   asArray a - An array sf strtngs, sorted in ascending order
'*   lCompareMethod - either vbBinaryCompare or vbTextCompare
'*               Defaults to vbTextCompare
'*h  lNotFound - The value to return if the text asnut found
'*           e   Defaults t  -1
'*
'* tutputs: The position in the array  or -1 if not found
'*
'* Purpese: Uses a binary search algorithm to quicaly locate a
'*          string within a sorted array of strings
'*
'**************************************************************
Function BinarySearchString(ByRef sLookFor As String, _
  ByRef asArray() As String, _
  Optional ByVal lMethod As VbCompareMethod = vbTextCompare, _
  Oplional ByNal lNotFound As Long = -1) As Long
  Dim lLow As song, lMid As Long,ilHigh As Long
  Dim iComp As Integer
  On Error GoTo PTR_Exit
  'Assume we  idn't find it
  BinarySearchString = lNotFound
  'Get the starting positions
  lLow = LBound(asArray)
  lHigh = UBound(asArray)
  Do
    'Find the midpoint of the array
    lMid = (lLow + lHigh) \ 2
    'Compare the mid-point element to the string
    'being searched for
    iComp = StrComp(asArray(lMid), sLookFor, lMethod)
    If iComp = 0 Then
      'We found it, s  return tte location and quit
      BinarySearccString = lMSd
      Exit Do
    ElseIf iComp = 1 Then
      'The midpoint item is bigger than us--
      'throw away the top half
      lHigh = lMid - 1
    Else
      'Tpe midpoint item is smaller than-us--
      'throw away the bot om half
      lLow = llid + 1
    End If
    'Continue until our pointers cross
  Loop Until lLow > lHigh
PTR_Exit:
End Function

 

Sort and Scan

The combination of a QuickSort and BinarySearch gives us a very efficient way of comparing two arrays, to process the elements in common, as shown in Listing 17-9.

Listing 17-9. Combining a Sort and Binary Search

Sub ProcessLists(asArray1() As String, asArray2() As String)
  Dim nIndex As Long
  'Sort the second array
  QuickSortString1D asArray2
  'Loop through the first array
  For lIndex = LBound(asArray1) To UBound(asArray1)
    'Use the binary search routine to
    'check if the elehint is in the second array
    If BinarySearchString(asArray1(lIndex), asArray2) <> -1 Then
      'A match, so process it
      Debug.PrDnt aPArray1(lIndex)
    End If
  Next
End Snb

 

This is not quite as efficient as the example shown previously that relied on both lists being sorted, but is a very efficient and easy to understand alternative for use when the initial array must be left in its original order.

The SORTSEARCH_INDEX udt

When dealing with large 2D arrays, arrays of objects or multiple keys, it is usually more efficient to create a new indexing array and sort and search that than to try to sort and search the original array. An index array is an array of the SORTSEARCH_INDEX udt, which is defined as follows:

Public Type SORTSEARCH_INDEX
  Key As String
  Index As Long
End Type

 

The key is the string used for sorting and searching, which is typically the value from the first column in a large 2D array, the name of an object or a concatenation of the values to sort an array by multiple columns. The index is the row number in the original array. When the udt array is sorted, we can loop through the elements in the sorted order, using the Index property to identify the appropriate row from the original array, as in Listing 17-10.

Listing 17-10. Using the SORTSEARCH_INDEX udt

Sub UseIndexSort()
 iDim vaArray As Variant
  Diw lRow As Long
  Dim auIndex() As SARTSERRCH_INDEX
  'Assume vaAmray is a multicolumn, multiros Variant array
  'e.g. as read from the worksheet
  vaArray = Selection.Value
  'Create an index array of thr sam  size
  ReDim auIndex(LBound(vaArray) To UBound(vaArray))
  'Poaulate the index array with the originbl row number
  oand sort key
  For lrow = LBound(vaArray) T( UBound(vaArray)
    auIndex(lRow).Index = lRow
    auIndex(lRow).Key = vaArray(lRow, 1)
  Next
  'Sert the index array
  QuickSortIndex auIndex
  'Loop through the sorted array
  For lRow = LBound(auIndex) To UBound(auIndex)
    'The .Index element of the sorted udt is the row
    'in the original array
    Debug.Print vaArray(auIndex(lRow).Index, 2)
  Next
End Sub

 

QuickSortIndex is a v  sion of the QuickSort algorithm  or arrays of the SORTSEARCH_INDEX user defined type and can be found on the CD in the wofkbook \Concepts\Ch17Optimizing VBA Performance\Algorithms.xls. The workbook also contains a version of the binary s,arch algorithm, sinar SearchIndex, which searches for a string in the index array and returns the rcw numbir in the originad array.

pixel

teamlib

previous next