1

As far as i know, monitoring exception will make a program slower.

Would an iterator exception monitor, such as StopIteration make a for loop slower?

twasbrillig
  • 17,084
  • 9
  • 43
  • 67
user890973
  • 947
  • 2
  • 8
  • 12
  • I fail to see the point: where is this `StopIteration` supposed to come from? Normally, it is monitored by the iteraator itself. – glglgl Nov 21 '13 at 09:12
  • @glglgl Yes,it's monitored by the iterator..So I'm don't understand why not iterator use a hasnext function instead of raise StopIteration Exception?use iterator is so common,I think it will improve performance a lot – user890973 Nov 21 '13 at 09:17
  • 2
    Unlike other languages like C++ where using exceptions is an order of magnitude slower than regular code, exception handling in Python is about the same speed as any other code. I would not worry about it unless you've done profiling that tells you that there is an issue for your specific use. – Blckknght Nov 21 '13 at 09:30
  • @Blckknght cool,would you mind share the reason why python's exception handling so fast. – user890973 Nov 21 '13 at 09:38

2 Answers2

10

While exception monitoring has some small overhead in the usual case, in the case of iterators there does not appear to be any overhead involved in handling StopIteration exceptions. Python optimises iterators as a special case so that StopIteration doesn't involve any exception handlers. (I'll also observe---and I may be missing something---that it's hard to come up with a Python for loop that doesn't implicitly use iterators).

Here's some examples, first using the built-in range function and a simple for loop:

Python 2.7.5
>>> import dis
>>> def x():
...   for i in range(1,11):
...     pass
...
>>> dis.dis(x)
  2           0 SETUP_LOOP              23 (to 26)
              3 LOAD_GLOBAL              0 (range)
              6 LOAD_CONST               1 (1)
              9 LOAD_CONST               2 (11)
             12 CALL_FUNCTION            2
             15 GET_ITER
        >>   16 FOR_ITER                 6 (to 25)
             19 STORE_FAST               0 (i)

  3          22 JUMP_ABSOLUTE           16
        >>   25 POP_BLOCK
        >>   26 LOAD_CONST               0 (None)
             29 RETURN_VALUE

Note that range is essentially being treated as an iterator.

Now, using a simple generator function:

>>> def g(x):
...   while x < 11:
...     yield x
...     x = x + 1
...
>>> def y():
...   for i in g(1):
...     pass
...
>>> dis.dis(y)
  2           0 SETUP_LOOP              20 (to 23)
              3 LOAD_GLOBAL              0 (g)
              6 LOAD_CONST               1 (1)
              9 CALL_FUNCTION            1
             12 GET_ITER
        >>   13 FOR_ITER                 6 (to 22)
             16 STORE_FAST               0 (i)

  3          19 JUMP_ABSOLUTE           13
        >>   22 POP_BLOCK
        >>   23 LOAD_CONST               0 (None)
             26 RETURN_VALUE
>>> dis.dis(g)
  2           0 SETUP_LOOP              31 (to 34)
        >>    3 LOAD_FAST                0 (x)
              6 LOAD_CONST               1 (11)
              9 COMPARE_OP               0 (<)
             12 POP_JUMP_IF_FALSE       33

  3          15 LOAD_FAST                0 (x)
             18 YIELD_VALUE
             19 POP_TOP

  4          20 LOAD_FAST                0 (x)
             23 LOAD_CONST               2 (1)
             26 BINARY_ADD
             27 STORE_FAST               0 (x)
             30 JUMP_ABSOLUTE            3
        >>   33 POP_BLOCK
        >>   34 LOAD_CONST               0 (None)
             37 RETURN_VALUE

Note that y here is basically the same as x above, the difference being one LOAD_CONST instruction, since x references the number 11. Likewise, our simple generator is basically equivalent to the same thing written as a while loop:

>>> def q():
...   x = 1
...   while x < 11:
...     x = x + 1
...
>>> dis.dis(q)
  2           0 LOAD_CONST               1 (1)
              3 STORE_FAST               0 (x)

  3           6 SETUP_LOOP              26 (to 35)
        >>    9 LOAD_FAST                0 (x)
             12 LOAD_CONST               2 (11)
             15 COMPARE_OP               0 (<)
             18 POP_JUMP_IF_FALSE       34

  4          21 LOAD_FAST                0 (x)
             24 LOAD_CONST               1 (1)
             27 BINARY_ADD
             28 STORE_FAST               0 (x)
             31 JUMP_ABSOLUTE            9
        >>   34 POP_BLOCK
        >>   35 LOAD_CONST               0 (None)
             38 RETURN_VALUE

Again, there's no specific overhead to handle the iterator or the generator (range may be somewhat more optimised than the generator version, simply because its a built-in, but not due to the way Python handles it).

Finally, let's look at an actual explicit iterator written with StopIteration

>>> class G(object):
...   def __init__(self, x):
...     self.x = x
...   def __iter__(self):
...     return self
...   def next(self):
...     x = self.x
...     if x >= 11:
...       raise StopIteration
...     x = x + 1
...     return x - 1
...
>>> dis.dis(G.next)
  7           0 LOAD_FAST                0 (self)
              3 LOAD_ATTR                0 (x)
              6 STORE_FAST               1 (x)

  8           9 LOAD_FAST                1 (x)
             12 LOAD_CONST               1 (11)
             15 COMPARE_OP               5 (>=)
             18 POP_JUMP_IF_FALSE       30

  9          21 LOAD_GLOBAL              1 (StopIteration)
             24 RAISE_VARARGS            1
             27 JUMP_FORWARD             0 (to 30)

 10     >>   30 LOAD_FAST                1 (x)
             33 LOAD_CONST               2 (1)
             36 BINARY_ADD
             37 STORE_FAST               1 (x)

 11          40 LOAD_FAST                1 (x)
             43 LOAD_CONST               2 (1)
             46 BINARY_SUBTRACT
             47 RETURN_VALUE

Now, here we can see that the generator function involves a few less instructions than this simple iterator, mostly related to the differences in implementation and a couple of instructions related to raising the StopIteration exception. Nevertheless, a function using this iterator is exactly equivalent to y above:

>>> def z():
...   for i in G(1):
...     pass
...
>>> dis.dis(z)
  2           0 SETUP_LOOP              20 (to 23)
              3 LOAD_GLOBAL              0 (G)
              6 LOAD_CONST               1 (1)
              9 CALL_FUNCTION            1
             12 GET_ITER
        >>   13 FOR_ITER                 6 (to 22)
             16 STORE_FAST               0 (i)

  3          19 JUMP_ABSOLUTE           13
        >>   22 POP_BLOCK
        >>   23 LOAD_CONST               0 (None)
             26 RETURN_VALUE

Of course, these results are based around the fact that Python for-loops will optimise iterators to remove the need for explicit handlers for the StopIteration exception. After all, StopIteration exception essentially form a normal part of the operation of a Python for-loop.


Regarding why it is implemented this way, see PEP-234 which defines iterators. This specifically addresses the issue of the expense of the exception:

  • It has been questioned whether an exception to signal the end of the iteration isn't too expensive. Several alternatives for the StopIteration exception have been proposed: a special value End to signal the end, a function end() to test whether the iterator is finished, even reusing the IndexError exception.

    • A special value has the problem that if a sequence ever contains that special value, a loop over that sequence will end prematurely without any warning. If the experience with null-terminated C strings hasn't taught us the problems this can cause, imagine the trouble a Python introspection tool would have iterating over a list of all built-in names, assuming that the special End value was a built-in name!

    • Calling an end() function would require two calls per iteration. Two calls is much more expensive than one call plus a test for an exception. Especially the time-critical for loop can test very cheaply for an exception.

    • Reusing IndexError can cause confusion because it can be a genuine error, which would be masked by ending the loop prematurely.

ig0774
  • 39,669
  • 3
  • 55
  • 57
2

Looking at the output of the bytecode generated by a function with a try and except block, it looks like it would be slightly slower, however, this is honestly negligible in most circumstances, as it is extremely small as far as performance hit goes. I think the real thing to consider when doing an optimization like this would be scoping the exceptions properly.

Output of an example function with try/except block when compiled to bytecode:

Python 2.7.3 (default, Apr 10 2012, 23:31:26) [MSC v.1500 32 bit (Intel)] on win32
Type "copyright", "credits" or "license()" for more information.
>>> import dis
>>> def x():
    try:
        sd="lol"
    except:
        raise


>>> dis.dis(x)
  2           0 SETUP_EXCEPT            10 (to 13)

  3           3 LOAD_CONST               1 ('lol')
              6 STORE_FAST               0 (sd)
              9 POP_BLOCK           
             10 JUMP_FORWARD            10 (to 23)

  4     >>   13 POP_TOP             
             14 POP_TOP             
             15 POP_TOP             

  5          16 RAISE_VARARGS            0
             19 JUMP_FORWARD             1 (to 23)
             22 END_FINALLY         
        >>   23 LOAD_CONST               0 (None)
             26 RETURN_VALUE        
>>> 
IT Ninja
  • 6,174
  • 10
  • 42
  • 65
  • Yes,use try..except make a program slower.Why python use raise StopIteration for iterator end signal.Why don't just use a function hasnext which return boolean value? – user890973 Nov 21 '13 at 09:13