elif versus else if; is one faster than the other and looking at python assembly

Question:

Are these two code snippets exactly the same (as they would be in C++) or will they both produce slightly different running times?

first:

x = 'hello joe'

if x == 'hello':
  print('nope')
elif x == 'hello joe':
  print(x)

second:

x = 'hello joe'

if x == 'hello':
  print('nope')
else:
  if x == 'hello joe':
    print(x)

I wanted to find out myself, but I am not sure how I might go about watching this code run in its assembly form in real time. Which brings me to my second question: how might I see the compiled assembly instructions that are made when I compile a Python program?

Asked By: Rob

||

Answers:

First, let’s put your code(s) in a function

def func():               # line 1
    x = 'hello joe'       # line 2

    if x == 'hello':      # line 4
      print('nope')       # line 5
    else:                 # line 6
     if x == 'hello joe': # line 7
      print(x)            # line 8

now disassemble with that (using CPython 3.4):

import dis
dis.dis(func)

we get:

  2           0 LOAD_CONST               1 ('hello joe')
              3 STORE_FAST               0 (x)

  4           6 LOAD_FAST                0 (x)
              9 LOAD_CONST               2 ('hello')
             12 COMPARE_OP               2 (==)
             15 POP_JUMP_IF_FALSE       31

  5          18 LOAD_GLOBAL              0 (print)
             21 LOAD_CONST               3 ('nope')
             24 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             27 POP_TOP
             28 JUMP_FORWARD            25 (to 56)

  7     >>   31 LOAD_FAST                0 (x)
             34 LOAD_CONST               1 ('hello joe')
             37 COMPARE_OP               2 (==)
             40 POP_JUMP_IF_FALSE       56

  8          43 LOAD_GLOBAL              0 (print)
             46 LOAD_FAST                0 (x)
             49 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             52 POP_TOP
             53 JUMP_FORWARD             0 (to 56)
        >>   56 LOAD_CONST               0 (None)
             59 RETURN_VALUE

now change to elif:

  2           0 LOAD_CONST               1 ('hello joe')
              3 STORE_FAST               0 (x)

  4           6 LOAD_FAST                0 (x)
              9 LOAD_CONST               2 ('hello')
             12 COMPARE_OP               2 (==)
             15 POP_JUMP_IF_FALSE       31

  5          18 LOAD_GLOBAL              0 (print)
             21 LOAD_CONST               3 ('nope')
             24 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             27 POP_TOP
             28 JUMP_FORWARD            25 (to 56)

  6     >>   31 LOAD_FAST                0 (x)
             34 LOAD_CONST               1 ('hello joe')
             37 COMPARE_OP               2 (==)
             40 POP_JUMP_IF_FALSE       56

  7          43 LOAD_GLOBAL              0 (print)
             46 LOAD_FAST                0 (x)
             49 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             52 POP_TOP
             53 JUMP_FORWARD             0 (to 56)
        >>   56 LOAD_CONST               0 (None)
             59 RETURN_VALUE

The only differences are the line numbers.

    else:                 # line 6
     if x == 'hello joe': # line 7

becomes (and shifts the rest as well)

    elif x == 'hello joe': # line 6

There are as many instructions in both versions. The else and if keywords in that case seem to have been converted exactly the same way as elif. Not guaranteed in all implementations. Personally I’d stick to the shortest code (elif) because it’s more “meaningful” and if a code should be faster, it would probably be that one.

As Jean-François Fabre already answered, both variants are equivalent in this very case (i.e., the bytecode is the same). In other cases where the resulting code should be checked for equality, we can do this by comparing its binary representation. A string can be compiled by dis.Bytecode(str), the binary representation is stored in attributes of codeobj.co_code.

first = """
x = 'hello joe'

if x == 'hello':
  print('nope')
elif x == 'hello joe':
  print(x)
"""

second = """
x = 'hello joe'

if x == 'hello':
  print('nope')
else:
  if x == 'hello joe':
    print(x)
"""

import dis

# binary representation of bytecode of compiled str 
def str_to_binary(f):
    return dis.Bytecode(f).codeobj.co_code

print(f"{str_to_binary(first) == str_to_binary(second) = }")

output:

str_to_binary(first) == str_to_binary(second) = True

In order to get the bytecode of a function it is not even necessary to import dis, you can access the code object via __code__, see the following example with two functions doing exactly the same thing:
1

def f(a):
    return a
    
def g(b):
    return b

# binary representation of a function's bytecode
def fun_binary(function):
    return function.__code__.co_code
    
print(f"{fun_binary(f) == fun_binary(g) = }")

output:

fun_binary(f) == fun_binary(g) = True

1 The functions f and g use different names for local variables which is reflected by the parenthesized information given by dis.dis() but the resulting byte code is identical for both functions.

Answered By: Wolf
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.