20

VBA doesn't short-circuit

VBA does not support short-circuiting - apparently because it only has bitwise And/Or/Not etc operations. From the VBA language specification: "Logical operators are simple data operators that perform bitwise computations on their operands." In this light, it makes sense that VBA was designed with true = &H1111 and false = &H0000: this way logical statements can be evaluated as bitwise operations.

The lack of short-circuiting can cause problems

  1. Performance: the ReallyExpensiveFunction() will always be run when this statement is evaluated, even if it is not necessary by the result of the left hand side of the condition

    If IsNecessary() And ReallyExpensiveFunction() Then '... End If

  2. Errors: if MyObj is Nothing, this conditional statment will result in a runtime error because VBA will still try to check the value of Property

    If Not MyObj Is Nothing And MyObj.Property = 5 Then '... End If

The solution I've used to implement short-cirtcuiting behavior is nested Ifs

If cond1 And cond2 Then
    '...
End If

Becomes

If cond1 Then
    If cond2 Then
        '...
    End If
End If

This way the If statements give the short-circuit-like behavior of not bothering to evaluate cond2 if cond1 is False.

If there is an Else clause, this creates duplicate code blocks

If Not MyObj Is Nothing And MyObj.Property = 5 Then
    MsgBox "YAY"
Else
    MsgBox "BOO"
End If

Becomes

If Not MyObj Is Nothing Then
    If MyObj.Property = 5 Then
        MsgBox "YAY"
    Else
        MsgBox "BOO" 'Duplicate
    End If
Else
    MsgBox "BOO" 'Duplicate
End If

Is there a way to rewrite If statements to preserve the short-circuit behavior, but avoid duplication of code?

Perhaps with another branching statement like Select Case?


To add context to the question, here is the specific case I'm looking at. I'm implementing a hash table that handles collisions by chaining them in a linked list. The underlying array size is enforced to be a power of two and the hashes are distributed into the current array size by truncating them to the appropriate length.

For example, suppose the array length is 16 (binary 10000). If I have a key that hashes to 27 (binary 11011), I can store it in my 16 slot array by keeping only the bits within the limit of that array size. The index where this item would be stored is (hash value) And (length of array - 1) which in this case is (binary 11011) And (1111) which is 1011 which is 11. The actual hash code is stored along with the key in the slot.

When looking up an item in the hash table in a chain, both the hash and the key must be checked to determine that the correct item has been found. However, if the hash doesn't match, then there is no reason to check the key. I was hoping to gain some tiny intangible amount of performance by nesting the Ifs to get the short-circuit behavior:

While Not e Is Nothing
    If keyhash = e.hash Then
        If Key = e.Key Then
            e.Value = Value
            Exit Property
        Else
            Set e = e.nextEntry
        End If
    Else
        Set e = e.nextEntry
    End If
Wend

You can see the Set... is duplicated, and thus this question.

Blackhawk
  • 5,984
  • 4
  • 27
  • 56
  • 3
    Perhaps a naive question, but can you not move the `SET` line outside the ifs (and inside the while before the bottom line)? You don't `SET` only when you exit, otherwise you do set. Good question by the way! – Ioannis Jul 08 '14 at 23:21
  • @Ioannis - (slapping hand on head). Please put that as an answer. :) – hnk Jul 08 '14 at 23:44
  • @loannis I can't do that, it makes WAAAAY too much sense ;) Thanks for pointing out the real solution to my specific issue, making the change to my code now... I'll let the rest of the question stand on its own for posterity. – Blackhawk Jul 09 '14 at 14:03

3 Answers3

13

As a more general apprach, I suggest to introduce condition flags and make usage of assigning comparison results to booleans:

dim cond1 as boolean
dim cond2 as boolean

cond1 = false
cond2 = false

' Step 1
cond1 = MyObj Is Nothing

' Step 2: do it only if step 1 was sucessful 
if cond1 then
    cond2 = MyObj.Property = 5
end if

' Final result:
if cond2 then
   msgbox "Yay"
else
   msgbox "Boo"
end if

By "chaining" those condition flags, every step is safe, you see the final result in the last condition flag and you don't do unnecessary comparisons. And, to me, it keeps readable.

EDIT 2014-07-09

I usually never omit block delimiters and I consequently set every statement of control structures on a new line. But in this case, you can carefully get a very dense notation that reminds on short-circuit notation, also because the VBA compiler initiates the variables:

dim cond1 as boolean
dim cond2 as boolean
dim cond3 as boolean
dim cond4 as boolean

cond1 = MyObj Is Nothing
if cond1 then cond2 = MyObj.Property = 5
if cond2 then cond3 = MyObj.Property2 = constSomething
if cond3 then cond4 = not isNull(MyObj.Property77)

if cond4 then
   msgbox "Hyper-Yay"
else
   msgbox "Boo"
end if

I could agree to this. It's a clear flow to read.

EDIT 2021-03-21

Thanks to @Tom's comment, one can write it simpler:

dim cond as boolean

cond = MyObj Is Nothing
if cond then cond = MyObj.Property = 5
if cond then cond = MyObj.Property2 = constSomething
if cond then cond = not isNull(MyObj.Property77)

if cond then
   msgbox "Hyper-Yay"
else
   msgbox "Boo"
end if

@Tom explains the advantages in his comment below. I fully agree with this. I can only imagine some situations while debugging, when I would like to have separated results of the conditions, and therefore explicitely with four different variables.

peter_the_oak
  • 3,529
  • 3
  • 23
  • 37
  • I like this. Condition flags where applicable, nested `Ifs` sometimes, functions and subroutines that take arguments `ByRef` instead of `ByVal`, etc. there are many ways to get around this but they all depend on the particular circumstances. – David Zemens Jul 08 '14 at 22:04
  • 2
    Thanks. I agree that there are even more approaches and it depends on the situation. Hmmm... Also a generic `function IfShortcut(ParamArray conditions()) as boolean ...` would be nice... In that condition, you can loop and `exit`. – peter_the_oak Jul 08 '14 at 22:15
  • Hmmm... Somehow you have to check the very final state. You could prepare the final argument like Tim Burton does and try avoiding an if. But the pattern requires somhow a final check. – peter_the_oak Jul 08 '14 at 22:43
  • I would use a single boolean variable for all the expressions. a) There's no significant (or any) need to know prior the expressions' results (and you wouldn't have that even in a language that *did* do short-circuiting). b) It eliminates the significant possibility of fat-fingering the wrong boolean variable suffix in: b.1) the final If statement, b.2) each lead-up If statement and b.3) each lead-up assignment statement. c) It eliminates the need to declare an extra boolean variable for each short-circuited expression. – Tom Feb 22 '21 at 15:15
  • 1
    Thank you @Tom, I've included your hint in the answer. – peter_the_oak Mar 14 '21 at 11:27
  • Re. "I can only imagine some situations while debugging, when I would like to have separated results of the conditions, and therefore explicitely with four different variables.": Since each expr is assigned to a var, you can still get that by setting breakpoints or stepping through each lead-up If stmt, you just can't wait until *after* next / all exprs are evaluated to check a prior one's value. That's one advantage that coding it this way has over taking advantage of a short-circuit feature, if any (i.e. you can just set a Watch on the single Bool var (vs. each expr) and step through it). – Tom Mar 15 '21 at 18:28
6

There is a way. You're not guaranteed to like it. But this is one of those carefully constructed cases where Goto comes in handy

If Not MyObj Is Nothing Then
    If MyObj.Property = 5 Then
        MsgBox "YAY"
    Else
        Goto JUMPHERE
    End If
Else
JUMPHERE:
    MsgBox "BOO" 'Duplicate
End If

A short-circuited code to implement a short-circuited condition!

Alternately, if instead of MsgBox "BOO" is some long and convoluted code, it can be wrapped in a function and that can be written twice with minimal impact/overhead.


Regarding the specific use case, the multiple Set operations will have a minimal performance impact and hence, if one wants to avoid using Goto (still the most globally efficient approach, codesize + performance wise, avoiding creation of dummy variables, etc. - won't matter, though for such a small piece of code) there is negligible downside in simply repeating the command.

Just to analyze (your sample code) how much can be gained by different methods...

  • If both conditions are true:, there are 2 comparisons, 1 assignment, 0 jumps
  • If only first condition is true: there are 2 comparisons, 1 pointer-assignment, 1 jump
  • If only second condition is true: there is 1 comparison, 1 pointer-assignment, 1 jump
  • If both conditions are false: there is 1 comparison, 1 pointer-assignment, 1 jump (same as above)

In terms of performance, a jump is usually more expensive than comparison (which happens very quickly in ALU vs. the jump which could lead to a disruption in the code cache, maybe not at these sizes, but still jumps are expensive).

And normal assignment by value would be at best as fast as a pointer-assignment or sometimes worse (this is VBA, can't be 100% sure of the p-code implementation)

So, depending on your use case / expected data, you can try to minimize average number of jumps per iteration in your loop and reorder the code.

hnk
  • 2,216
  • 1
  • 13
  • 18
  • 1
    Function wrappers +1. I use them (perhaps excessively) but it really does help keep your code readable and easier to maintain. – David Zemens Jul 08 '14 at 22:06
  • 2
    +1 "You're not guaranteed to like it" :P This definitely would solve it, but yeah that `Goto` leaves me plenty of room to screw my code up if I'm not careful. Would jumping out of branch or loop statements ever cause problems you think? – Blackhawk Jul 08 '14 at 22:08
  • 2
    No, interally C++/VBA's p-code, etc. convert your branches to Goto anyways, so there is no implementation issue. Only a Philosophical one. But the argument (Djikstra's mainly) against Goto was from the point of view of avoiding spaghetti code. The way I do it for any optimization is, I write the neat code first. Then comment it all out, and put the hyperoptimized one next to it. That way, read the regular code for logic and debugging, and hyperoptimized and thoroughly vetted for real performance. In this case, in particular, Goto makes it easier to read - despaghettizes the code! – hnk Jul 08 '14 at 22:11
  • 3
    For the purists: you can use a boolean flag, instead of a GOTO, to get the same results. – Joel Coehoorn Jul 08 '14 at 22:27
  • @JoelCoehoorn, True, but perhaps once the Boolean Flag is set, another branch operation would be needed which would be detrimental to performance and compared to that the function wrapper might be slightly more efficient if parameters are passed as references. – hnk Jul 08 '14 at 22:32
  • Because of the behavior of hash tables, the two most common paths will be that both conditions are false or both conditions are true. Hopefully I've chosen my hash function so that collisions will be minimized. You bring up an interesting point ala [Branch Prediction](http://stackoverflow.com/a/11227902/2832561) (one of the highest voted answers I've seen!). – Blackhawk Jul 09 '14 at 14:12
  • Agree regd. Branch prediction. I often write code to implement numerical methods (certainly not in VBA), and then need to optimize the living daylights out of the innermost loops. And every if/then condition counts there so every little bit-hack to avoid branching or branchless comparison operation makes a perceptible difference in system performance! Regd your code, with hash tables you should be fine with the short-circuit being used only sparingly. – hnk Jul 09 '14 at 14:22
  • 1
    Valid use of GoTo.... I hate to say it, but ++. – RubberDuck Oct 20 '14 at 20:28
0

How about:

s = "BOO"

If Not MyObj Is Nothing Then
    If MyObj.Property = 5 Then s = "YAY"
End If

MsgBox s
Tim Williams
  • 154,628
  • 8
  • 97
  • 125
  • But "BOO" might be the 'expensive' function which you would like to avoid executing if possible in the first place. Would there be an alternate workaround for that case? – hnk Jul 08 '14 at 21:58
  • Unfortunately in the actual case I'm dealing with, the Else clause actually has a `Set` assignment :( Still, something like this would definitely work there, but I'm not sure if it would cancel the tiny performance gain I was going for with the nested `If`s. I'll modify the question and add the specific case for context. See what you think! – Blackhawk Jul 08 '14 at 22:05
  • I know this is really old, but for future readers...all that would be necessary here to avoid the expensive function as well as the messier version (such as GoTo) is checking for the null version of the variable we are trying to make. In this case `If s = vbNullString Then s = ExpensiveFunction`. Clean, and logical. – Brandon Barney Oct 10 '17 at 13:05