1

I have read several thread in Stackoverflow. Many claim that using a dict is faster than if-elif-else statements in Python. I made some test using Python 3.7.4 and got different results.

I have built a test script. I used timeit to measure my code. I'm building an interpreter of pseudocode. My interpreter uses if-elif-else statements to find the python procedure to interpret an specific pseudocode. But I wanted to move to a dict approve and made some testing.

from timeit import Timer


def add(x, y):
    return x + y


def sub(x, y):
    return x - y


def mul(x, y):
    return x * y


def div(x, y):
    return x / y


def foo1(oper, a, b):
    return funcs[oper](a, b)


def foo2(oper, a, b):
    if oper == 'add':
        return a + b
    elif oper == 'sub':
        return a - b
    elif oper == 'mul':
        return a * b
    else:
        return a / b


def foo3(oper, a, b):
    subfuncs[oper](a, b)


funcs = {'add': lambda x, y: x + y,
         'sub': lambda x, y: x - y,
         'mul': lambda x, y: x * y,
         'div': lambda x, y: x / y}

subfuncs = {'add': add,
            'sub': sub,
            'mul': mul,
            'div': div}

times_to_run = 10000000
t1 = Timer("foo1('div', 3, 2)", "from __main__ import foo1")
t2 = Timer("foo2('div', 3, 2)", "from __main__ import foo2")
t3 = Timer("foo3('div', 3, 2)", "from __main__ import foo3")
tot1 = t1.timeit(times_to_run)
tot2 = t2.timeit(times_to_run)
tot3 = t3.timeit(times_to_run)
print("Time for foo1 is: {:4.2f}".format(tot1))
print("Time for foo2 is: {:4.2f}".format(tot2))
print("Time for foo3 is: {:4.2f}".format(tot3))

if tot1 > tot2:
    res1 = 'slower'
    times1 = '{:6.2f}'.format((tot1 / tot2 - 1) * 100)
elif tot1 < tot2:
    res1 = 'faster'
    times1 = '{:6.2f}'.format((tot2 / tot1 - 1) * 100)
else:
    res1 = 'equal'
    times1 = ''
print("\nfoo1 is {}% {} in comparison to foo2".format(times1, res1))

if tot2 > tot3:
    res2 = 'slower'
    times2 = '{:6.2f}'.format((tot2 / tot3 - 1) * 100)
elif tot2 < tot3:
    res2 = 'faster'
    times2 = '{:6.2f}'.format((tot3 / tot2 - 1) * 100)
else:
    res2 = 'equal'
    times2 = ''
print("foo2 is {}% {} in comparison to foo3".format(times2, res2))

if tot1 > tot3:
    res3 = 'slower'
    times3 = '{:6.2f}'.format((tot1 / tot3 - 1) * 100)
elif tot1 < tot3:
    res3 = 'faster'
    times3 = '{:6.2f}'.format((tot3 / tot1 - 1) * 100)
else:
    res3 = 'equal'
    times3 = ''
print("foo1 is {}% {} in comparison to foo3".format(times3, res3))

I was expecting that foo3 would be the fastest function, instead foo2 is the fastest every time. I always get output similar to this, and outputs are always consistence to this output:

Time for foo1 is: 3.35
Time for foo2 is: 2.99
Time for foo3 is: 3.06

foo1 is  12.18% slower in comparison to foo2
foo2 is   2.51% faster in comparison to foo3
foo1 is   9.44% slower in comparison to foo3

My question is why foo2, which has if-elif-else statements is faster than using foo3 which uses a dict of functions?

PS. This is not my actual code. I'm testing which approach would be faster.

1 Answers1

1

My question is why foo2, which has if-elif-else statements is faster than using foo3 which uses a dict of functions?

Well, in foo3, you have one global name lookup (your function uses a global dict), one dict lookup (=> one attribute lookup for the __getitem__ method + one method call to __getitem__), and one function call.

In foo2, you have no global lookup, no dict lookup, and no function call at all since all operations are inlined, so this largely makes up for the time lost testing conditions sequentially (which is the reason why dict lookups can, sometimes be faster than branching), specially when you only have 4 conditions...

If you want your test to be relevant, you should at least rewrite foo2 using function calls:

def foo2(oper, a, b):
    if oper == 'add':
        return add(a, b)
    elif oper == 'sub':
        return sub(a, b)
    elif oper == 'mul':
        return mul(a, b)
    elif oper == 'div':
        return div(a, b)
    else:
        raise ValueError("unknown operation '{}'".format(oper))

(and make sure you don't always test it using 'add' of course - but here your test is mostly ok as it uses "div" which is the last condition so theoretically the worst case).

Many claim that using a dict is faster than if-elif-else statements in Python

If you read related questions (and answers) like this one, you'll find out that it really depends on more than just branching vs dict lookup (how many branches / dict keys, what your code is effectively doing etc etc etc).

bruno desthuilliers
  • 75,974
  • 6
  • 88
  • 118
  • Thank you, this explains very well. I was not comparing apples to apples. With your suggestion I got that foo3 is the fastest and foo2 is the slowest, and foo1 a close competitor to foo3. `Time for foo1 is: 3.13 Time for foo2 is: 4.46 Time for foo3 is: 3.03` – Luis Ramirez Jul 22 '19 at 15:10
  • 1
    Proper benchmarking is not trivial indeed, even for seemingly simple cases it's too easy to overlook the one simple thing that skew the results... – bruno desthuilliers Jul 22 '19 at 15:15