2

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 condition2b if condition2a 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
17
  • 1
    if you share you code maybe your If of other loops can be replaced with Select Case, and Match , etc.. Commented Feb 7, 2017 at 9:34
  • 2
    I think your answer is here: stackoverflow.com/a/7015575/78522 Commented Feb 7, 2017 at 9:36
  • Also some interesting workarounds here: stackoverflow.com/q/3242560/78522 Commented Feb 7, 2017 at 9:38
  • 1
    in VBA If statement doesn't short circuit. You may want to use Select Case construct or, if all those conditions are about selecting records from a database, filter the database (e.g.: with AutoFilter() or the likes) Commented Feb 7, 2017 at 9:41
  • 1
    @Nathan_Sav although the ((1=1)+(2=3)) will be equal to ((1=1) or (2=3)) because True=-1 and False=0 but it will not prevent the second part of condition to be checked (no performance gain) Commented Feb 7, 2017 at 11:03

1 Answer 1

3

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 ifs:

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
Sign up to request clarification or add additional context in comments.

8 Comments

Regarding the array redim point, I actually ran a test (called 5000 times) where I created once a 10000-array VS redim the array every line OR redim every 1000 lines (If Int(l / 1000) * 1000 = l Then ReDim Preserve tableau3(l * 10)). Redim every time is for sure the worst performing option. But since, I use the size of the array to filter the results, pre-dimensioning is not an option for me.
Regarding Select Case, the 24-deep nested If statement code is already hardly readable. Nesting some Case Select inside the If... I'm not sure I'm even willing to do so...
Keep the actual upper bound index in a variable. At the beginning, my code was doing so, but since I used this array a lot (in different sub, modules and userform), I got rid of my array_size variable and switched to Ubound... :-(
about the 24 nested ifs, i can't do anything without seeing them if they could be merged or revised for better performance. you need to convert just those that have some OR into the select case (not all of them) and this is the cost you should pay to gain better results. regarding the array_size variable, it is the same: to have better performance you have to write some more complex code (although i don't think using such variable be so hard if implemented correctly). but the final improvement effect depends on many things in your scenario to be measured by you.
OK, so I just modify the code to totally avoid the array redim (by declaring an fixed-sized (500) array and exiting if more than 500), but there is almost no gain in performance, since most of the time (>90%), the redim is called only once (which is what I'm trying to achieve with the nested if statement) ! But thanks for the input. (Actually, I think I might even exit conditionnaly if Ubound >1...)
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.