Igglyboo
Igglyboo

Reputation: 845

Does the "else" statement have any overhead?

Is there any performance difference between this:

for item in collection:
    if item == badItem:
        break
    else
        doFunction(item)

And this:

for item in collection:
    if item == badItem:
        break
    doFunction(item)

Assume I'm doing this a couple hundred million times so any performance difference will help.

EDIT: I'm not actually implementing this based off the results of this question, I'm just wondering theoretically what is faster. I'm just curious.

Upvotes: 0

Views: 257

Answers (4)

Henry Keiter
Henry Keiter

Reputation: 17188

Grant's answer has it right: if you're concerned about performance, first get the code running, then measure what needs to be improved, and finally improve that stuff.

For posterity, here are my timing results. The short answer: there's no real difference, even over billions of iterations.

With Else:
min: 0.001327058799944325
max: 0.0037289344766406884
mean: 0.002665085947631951

Without Else:
min: 0.0013189987034252226
max: 0.003550914613782652
mean: 0.002147321588495288

And the code:

C:\>python
Python 3.3.2 (v3.3.2:d047928ae3f6, May 16 2013, 00:03:43) [MSC v.1600 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> else_version = timeit.repeat('for i in c:\n\tif i == 0: break\n\telse:i += 1','import random;c=[random.randint(0,1) for _ in range(1000000)]', number = 10000, repeat=10)
>>> nelse_version = timeit.repeat('for i in c:\n\tif i == 0: break\n\ti += 1','import random;c=[random.randint(0,1) for _ in range(1000000)]', number = 10000, repeat=10)
>>> min(else_version)
0.001327058799944325
>>> max(else_version)
0.0037289344766406884
>>> sum(else_version)/10
0.002665085947631951
>>>
>>> min(nelse_version)
0.0013189987034252226
>>> max(nelse_version)
0.003550914613782652
>>> sum(nelse_version)/10
0.002147321588495288
>>>

Whatever the cost of having the else statement might actually be, it's clearly dwarfed by any actual operations you're doing (such as your __eq__ implementation, or what your actual doFunction is, or even just other stuff happening on your machine).

Upvotes: 1

georg
georg

Reputation: 215039

Here are dis'es of both versions, side by side:

 0 SETUP_LOOP              40 (to 43)      |  0 SETUP_LOOP              40 (to 43)
 3 LOAD_GLOBAL              0 (collection) |  3 LOAD_GLOBAL              0 (collection)
 6 GET_ITER                                |  6 GET_ITER            
 7 FOR_ITER                32 (to 42)      |  7 FOR_ITER                32 (to 42)
10 STORE_FAST               0 (item)       | 10 STORE_FAST               0 (item)
                                           | 
13 LOAD_FAST                0 (item)       | 13 LOAD_FAST                0 (item)
16 LOAD_GLOBAL              1 (badItem)    | 16 LOAD_GLOBAL              1 (badItem)
19 COMPARE_OP               2 (==)         | 19 COMPARE_OP               2 (==)
22 POP_JUMP_IF_FALSE       29              | 22 POP_JUMP_IF_FALSE       29
                                           | 
25 BREAK_LOOP                              | 25 BREAK_LOOP          
26 JUMP_ABSOLUTE            7              | 26 JUMP_FORWARD             0 (to 29)
                                           | 
29 LOAD_GLOBAL              2 (doFunction) | 29 LOAD_GLOBAL              2 (doFunction)
32 LOAD_FAST                0 (item)       | 32 LOAD_FAST                0 (item)
35 CALL_FUNCTION            1              | 35 CALL_FUNCTION            1
38 POP_TOP                                 | 38 POP_TOP             
39 JUMP_ABSOLUTE            7              | 39 JUMP_ABSOLUTE            7
42 POP_BLOCK                               | 42 POP_BLOCK           
43 LOAD_CONST               0 (None)       | 43 LOAD_CONST               0 (None)
46 RETURN_VALUE                            | 46 RETURN_VALUE        

As you can see, the only difference is JUMP_ABSOLUTE (with else) vs. JUMP_FORWARD (without it). Since both opcodes are immediately after BREAK_LOOP, they won't run in any case, so both versions are fully equivalent.

That said, an else after a breaking statement (break/continue/return) is usually considered a code smell (and takes an extra useless line).

If you're interested in maximal performance, it might be worth considering .index or itertools.takewhile instead of a plain loop with if.

Upvotes: 5

Hyperboreus
Hyperboreus

Reputation: 32449

As Grant Birchmeier said: measure, measure, measure.

On my box using Python 3.3.1 (default, Apr 17 2013, 22:30:32) [GCC 4.7.3] on linux I get these results:

testA 0.7911653139999544
testB 0.7868194140028208
testC 0.7771379340010753

Using:

collection = [random.randint (1, 10000000) for _ in range (10000000) ]
badItem = 0
collection [5000000] = 0

def doFunction (item): pass

def testA ():
    for item in collection:
        if item == badItem: break
        else: doFunction (item)

def testB ():
    for item in collection:
        if item == badItem: break
        doFunction (item)

def testC ():
    badIndex = collection.index (badItem)
    for item in collection [:badIndex]:
        doFunction (item)

YMMV. I am just comparing ints and no real world data. I have no idea how costly is your __eq__, what doFunction does, etc.

Upvotes: 1

Grant Birchmeier
Grant Birchmeier

Reputation: 18504

This sounds like pre-mature optimization: Don't do it.

You should make your program work correctly before you try to optimize it, if you even have to.

If your finished app turns out to be slower than you need, then measure, measure, measure. Use a profiling tool. The parts that are slow will probably surprise you. Don't waste time fixing parts that aren't provably slow.

But back to the first point: Don't try to optimize a program that isn't feature-complete.

Upvotes: 2

Related Questions