简体   繁体   中英

VBA : Performance on 24-deep nested IF statement

Original question

I have a sub with a 24-deep nested IF statement

l=2
While l <= lmax                'lmax = 15000
   If condition1 Then
      If condition2a or condition2b Then
         ...
            If condition24 then
               ReDim Preserve propositions(UBound(propositions) + 1)
               propositions(UBound(propositions)) = l

Since this sub is called 250 times, the IF statement get called 250 * 15000, thus performance is a big issue. (The macro run in about 23 seconds.)

When I write If condition2a or condition2b Then , does VBA check condition 2b if condition 2a is true ? (Ie, should ordrer a and b so that a is less often true that b ?)

PS : Of course, condition 1 vs 2 are already ordered.

Short answer

As stated by @iDevlop, the short answer seems to be that VBA doesn't allow " short-circuit evaluation " ( See this post )

Solution to my performance problem

My problem was VBA reading/accessing data from the sheet (rather than VBA computing the IF statement.).

The solution was loading data in a 2D-array . This single modification make my Sub run more than 10 times quicker (less than 2s vs 23s).

Original code

Here is a shorter (17-deep) version of my statement :

With Sheets("Sheet1")
        lmax = .Cells(100000, 1).End(xlUp).Row    'Usually 14000
        l = 2
        While l <= lmax
            If boolean_ignore_param1 Or Left(.Cells(l, 1).Formula, Len(param1)) = param1 Then
                If boolean_ignore_param2 Or Left(.Cells(l, 2).Formula, Len(param2)) = param2Then
                    If (param_boolean_A And .Range("AF" & l).Formula = "Yes") Or (param_boolean_B And .Range("Ag" & l).Formula = "Yes") Then
                        If (.Cells(l, 6).Formula = "" Or .Cells(l, 6).Value - marge <= param3 Or param3= 0) Then
                        If (.Cells(l, 7).Formula = "" Or .Cells(l, 7).Value + marge >= param3 Or param3 = 0) Then
                        If (.Cells(l, 8).Formula = "" Or .Cells(l, 8).Value - marge <= param4 Or param4 = 0) Then
                        If (.Cells(l, 9).Formula = "" Or .Cells(l, 9).Value + marge >= param4 Or param4 = 0) Then
                        If (.Cells(l, 10).Formula = "" Or .Cells(l, 10).Value - marge <= param5 Or param5 = 0) Then
                        If (.Cells(l, 11).Formula = "" Or .Cells(l, 11).Value + marge >= param5 Or param5 = 0) Then
                        If (.Cells(l, 12).Formula = "" Or .Cells(l, 12).Value <= param6 Or param6 = 0) Then
                        If (.Cells(l, 13).Formula = "" Or .Cells(l, 13).Value >= param6 Or param6 = 0) Then
                        If (.Cells(l, 16).Formula = "" Or .Cells(l, 16).Value - marge <= param7 Or param7 = 0) Then
                        If (.Cells(l, 17).Formula = "" Or .Cells(l, 17).Value + marge >= param7 Or param7 = 0) Then
                        If (.Cells(l, 18).Formula = "" Or .Cells(l, 18).Value - marge <= param8 Or param8 = 0) Then
                        If (.Cells(l, 19).Formula = "" Or .Cells(l, 19).Value + marge >= param8 Or param8 = 0) Then
                        If (.Cells(l, 22).Formula = "" Or .Cells(l, 22).Value - marge <= param9 Or param9 = 0) Then
                        If (.Cells(l, 23).Formula = "" Or .Cells(l, 23).Value + marge >= param9  Or param9 = 0) Then
                            ReDim Preserve propositions(UBound(propositions) + 1)
                            propositions(UBound(propositions)) = l

instead of or, you can use Select Case with comma seperated list of conditions as in following:

'If condition2a Or condition2b Then

Select Case True
Case condition2a, condition2b 'here comma means lazy 'OR' (like as OrElse in vb.net)
  's = s + 10
Case Else
  's = s + 20
End Select

Also, there may be many points to improve your macro performance if we can see your code. instantly, the redim of array to add one more item to it may be time consuming in a loop:

ReDim Preserve propositions(UBound(propositions) + 1)

you may consider to increase its ubound as 10 or 100 items each time you reach its length (to reserve some space for next probable uses), but keep the actual upper bound index in a variable...


Update:

as you add some part of your code, i can suggest you to use some helper function for each if as following:

to replace x<param if 's:

If (.Cells(l, 6).Formula="" Or .Cells(l, 6).Value-marge<=param3 Or param3=0) Then ...

with something like as:

If test(.Cells(l, 6).Value, marge, param3) Then ...
'or without '.Value': If test(.Cells(l, 6), marge, param3) Then ...

we can define this function:

Function testLesser(v As Variant, marge As Double, param As Double) As Boolean

    'testLesser = (v = "" Or v - marge <= param3 Or param3 = 0)

    If v = "" Then
    ElseIf v - marge <= param Then
    ElseIf param = 0 Then
    Else
                testLesser = False: Exit Function
    End If
    testLesser = True

    '** Another option (using Select Case):
    'Select Case True
    'Case v = "", v - marge <= param, param = 0
    '    testLesser = True
    'Case Else
    '    testLesser = False
    'End Select

End Function

and similar for other type (greater than) of if s:

If (.Cells(l, 7).Formula="" Or .Cells(l, 7).Value+marge>=param3 Or param3=0) Then ...

we have:

Function testGreater(v As Variant, marge As Double, param As Double) As Boolean

    'testGreater = (v = "" Or v + marge >= param Or param = 0)

    If v = "" Then 'testLesser = True
    ElseIf v + marge >= param Then 'testLesser = True
    ElseIf param = 0 Then 'testLesser = True
    Else
                testLesser = False: Exit Function
    End If
    testLesser = True

    '** Another option (using Select Case):
    'Select Case True
    'Case v = "", v + marge >= param, param = 0
    '    testLesser = True
    'Case Else
    '    testLesser = False
    'End Select

End Function

So, the code will look like as:

'If (.Cells(l, 6).Formula = "" Or .Cells(l, 6).Value - marge <= param3 Or param3 = 0) Then
'If (.Cells(l, 7).Formula = "" Or .Cells(l, 7).Value + marge >= param3 Or param3 = 0) Then
'If (.Cells(l, 8).Formula = "" Or .Cells(l, 8).Value - marge <= param4 Or param4 = 0) Then
'If (.Cells(l, 9).Formula = "" Or .Cells(l, 9).Value + marge >= param4 Or param4 = 0) Then
'...

If testLesser(.Cells(l, 6), marge, param3) Then
If testGreater(.Cells(l, 7), marge, param3) Then
If testLesser(.Cells(l, 8), marge, param4) Then
If testGreater(.Cells(l, 9), marge, param4) Then
'...

My real test shows its faster! (and obviously, its also more readable code)

Note:

its very important to arrange the if conditions such that you get final condition as soon as you can! for example if cell values are usually empty, put that condition at first in our test function, but if param = 0 is generally true, bring it as first condition check...

this is the rule for x OR y criteria. for 'x AND y' criteria, it is the reverse! the most rare case must be at first to quickly filter the results. in your code, i see you arrange the nested if's from Cells(l, 6) to Cells(l, 23) . I don't know if this is best for your situation. it depends on your data and usual cases, so consider revising order of those nested if 's if you know some are usually false...

Another Tip:

instead of using With Sheets("Sheet1") , cache it within a variable, this can improve the performance!

Dim mySheet As Worksheet
Set mySheet = Sheets("Sheet1")
With mySheet 'Sheets("Sheet1")

my test shows this simple reference change is faster about 10% . you may think of other similar changes when working with sheets, ranges, cells...

Note: if we can define marge as global or sheet level var, we could remove it from function params but it seems that it doesn't have sensible effect...

Last Update:

as suggested by @Ioannis in comments (see also this ref) when working with a large range of cells, its better to load values into a 2D Array and using it instead of direct access to the Cells:

myArray = Sheets("Sheet1").Range("A1:AG15000").Value

then use that Array for read/writes as:

myArray(row, col) = myArray(row, col) + 1 
'row = 1 to UBound(myArray, 1) 'First array dimension is for rows
'col = 1 to UBound(myArray, 2) 'Second array dimension is for columns

finally when you finished you can update the entire range reversely:

Sheets("Sheet1").Range("A1:AG15000") = myArray

Not an answer support of a comment that caused confusion.

在此处输入图片说明

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM