Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Are compound if statements faster, or multiple if statements?

Say we have two pieces of code:

if foo == True and bar == False and baz == True:
    do something

and

if foo == True:
    if bar == False:
        if baz == True:
            do something

Which is faster?

like image 814
EducateMe Avatar asked Dec 25 '22 12:12

EducateMe


2 Answers

On my machine with IPython

In [1]: foo = True
In [2]: bar = False
In [3]: baz = True

In [4]: %%timeit
   ...: if foo and not bar and baz:
   ...:     lambda: None
1000000 loops, best of 3: 265 ns per loop

In [5]: %%timeit
   ...: if foo:
   ...:   if not bar:
   ...:     if baz:
   ...:       lambda: None
1000000 loops, best of 3: 275 ns per loop

It looks like there is a whopping 10ns overhead if you split it up. If 10ns matters, you should probably be using another language.

So for all practical purposes, no, there is no difference.

If we look a little deeper, we can see where that tiny difference comes from though.

In [6]: def compound():
   ...:     if foo and not bar and baz:
   ...:         lambda: None
In [7]: def multiple():
   ....:     if foo:
   ....:         if not bar:
   ....:             if baz:
   ....:                 lambda: None

In [8]: import dis
In [9]: dis.dis(compound)
  2           0 LOAD_GLOBAL              0 (foo)
              3 POP_JUMP_IF_FALSE       29
              6 LOAD_GLOBAL              1 (bar)
              9 UNARY_NOT           
             10 POP_JUMP_IF_FALSE       29
             13 LOAD_GLOBAL              2 (baz)
             16 POP_JUMP_IF_FALSE       29

  3          19 LOAD_CONST               1 (<code object <lambda> at 0x101d953b0, file "<ipython-input-9-d057c552d038>", line 3>)
             22 MAKE_FUNCTION            0
             25 POP_TOP             
             26 JUMP_FORWARD             0 (to 29)
        >>   29 LOAD_CONST               0 (None)
             32 RETURN_VALUE   

This has 13 instructions

In [15]: dis.dis(g)
  2           0 LOAD_GLOBAL              0 (foo)
              3 POP_JUMP_IF_FALSE       34

  3           6 LOAD_GLOBAL              1 (bar)
              9 POP_JUMP_IF_TRUE        34

  4          12 LOAD_GLOBAL              2 (baz)
             15 POP_JUMP_IF_FALSE       31

  5          18 LOAD_CONST               1 (<code object <lambda> at 0x101dbb530, file "<ipython-input-10-32b41e5f6f2b>", line 5>)
             21 MAKE_FUNCTION            0
             24 POP_TOP             
             25 JUMP_ABSOLUTE           31
             28 JUMP_ABSOLUTE           34
        >>   31 JUMP_FORWARD             0 (to 34)
        >>   34 LOAD_CONST               0 (None)
             37 RETURN_VALUE  

This has 14 instructions.

I did this with the default IPython on my system, which is 2.7.5 at the moment, but you can use this technique to profile pretty much anything you want with any version of Python you happen to be running.

like image 156
munk Avatar answered Jan 18 '23 23:01

munk


Let's examine the bytecode and see!

>>> def f():
...   if foo == True and bar == False and baz == True:
...     pass
...
>>> def g():
...     if foo == True:
...         if bar == False:
...             if baz == True:
...                 pass
...
>>> dis.dis(f)
  2           0 LOAD_GLOBAL              0 (foo)
              3 LOAD_GLOBAL              1 (True)
              6 COMPARE_OP               2 (==)
              9 POP_JUMP_IF_FALSE       39
             12 LOAD_GLOBAL              2 (bar)
             15 LOAD_GLOBAL              3 (False)
             18 COMPARE_OP               2 (==)
             21 POP_JUMP_IF_FALSE       39
             24 LOAD_GLOBAL              4 (baz)
             27 LOAD_GLOBAL              1 (True)
             30 COMPARE_OP               2 (==)
             33 POP_JUMP_IF_FALSE       39

  3          36 JUMP_FORWARD             0 (to 39)
        >>   39 LOAD_CONST               0 (None)
             42 RETURN_VALUE
>>> dis.dis(g)
  2           0 LOAD_GLOBAL              0 (foo)
              3 LOAD_GLOBAL              1 (True)
              6 COMPARE_OP               2 (==)
              9 POP_JUMP_IF_FALSE       45

  3          12 LOAD_GLOBAL              2 (bar)
             15 LOAD_GLOBAL              3 (False)
             18 COMPARE_OP               2 (==)
             21 POP_JUMP_IF_FALSE       45

  4          24 LOAD_GLOBAL              4 (baz)
             27 LOAD_GLOBAL              1 (True)
             30 COMPARE_OP               2 (==)
             33 POP_JUMP_IF_FALSE       42

  5          36 JUMP_ABSOLUTE           42
             39 JUMP_ABSOLUTE           45
        >>   42 JUMP_FORWARD             0 (to 45)
        >>   45 LOAD_CONST               0 (None)
             48 RETURN_VALUE

It's almost identical. The actual logic part is the exact same opcode sequence. It looks like the second version has very slightly less efficient jump targets, so g might run very slightly slower, but this might change between Python versions and it almost always won't matter.

like image 45
user2357112 supports Monica Avatar answered Jan 18 '23 22:01

user2357112 supports Monica