Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Exceptions catching performance in python

I know exceptions in python are fast when it comes to the try but that it may be expensive when it comes to the catch.

Does this mean that:

try:
   some code
except MyException:
   pass

is faster than this ?

try:
   some code
except MyException as e:
   pass
like image 956
maazza Avatar asked Apr 15 '16 08:04

maazza


People also ask

What is catching exceptions in Python?

Catching Exceptions in Python In Python, exceptions can be handled using a try statement. The critical operation which can raise an exception is placed inside the try clause. The code that handles the exceptions is written in the except clause.

Are exceptions slow in Python?

Demerits of Python Exception Handling Like, programs that make use try-except blocks to handle exceptions will run slightly slower, and the size of your code will increase.

What are the 3 major exception types in Python?

There are mainly three kinds of distinguishable errors in Python: syntax errors, exceptions and logical errors.

Which exception catch all exceptions in Python?

Try and Except Statement – Catching all Exceptions Try and except statements are used to catch and handle exceptions in Python. Statements that can raise exceptions are kept inside the try clause and the statements that handle the exception are written inside except clause.


3 Answers

The catch isn't expensive, the parts that appear relatively slow are the creation of the stack trace itself and if required the subsequent unwinding of the stack.

All stack based language that I'm aware of that allow you to capture stack traces need to perform these operations.

  1. When raise is called collect the stack information. Note, Java 1.7 allows you to suppress stack collection and it's a lot faster but you lose a lot of useful information. There's no sensible way for the language to know who will catch it so ignoring an exception does not help because it has to perform the bulk of the work anyway.
  2. If we're raising an exception then Unwind the stack ie deallocate all the memory and unwind back until we hit a valid catch.

The catch is minuscule in comparison to the above two operations. Here's some code to demonstrate that as the stack depth increases the performance goes down.

#!/usr/bin/env python
import os
import re
import time
import pytest

max_depth = 10
time_start = [0] * (max_depth + 1)
time_stop  = [0] * (max_depth + 1)
time_total = [0] * (max_depth + 1)
depth = []
for x in range(0, max_depth):
  depth.append(x)

@pytest.mark.parametrize('i', depth)
def test_stack(benchmark, i):
  benchmark.pedantic(catcher2, args=(i,i), rounds=10, iterations=1000)

#@pytest.mark.parametrize('d', depth)
#def test_recursion(benchmark, d):
#  benchmark.pedantic(catcher, args=(d,), rounds=50, iterations=50) 

def catcher(i, depth):
  try:
    ping(i, depth)
  except Exception:
    time_total[depth] += time.clock() - time_start[depth]

def recurse(i, depth):
  if(d > 0):
    recurse(--i, depth)
  thrower(depth)

def catcher2(i, depth):
  global time_total
  global time_start
  try:
    ping(i, depth)
  except Exception:
    time_total[depth] += time.clock() - time_start[depth]

def thrower(depth):
  global time_start
  time_start[depth] = time.clock()
  raise Exception('wtf')

def ping(i, depth):
  if(i < 1): thrower(i, depth)
  return pong(i, depth)

def pong(i, depth):
  if(i < 0): thrower(i,depth)
  return ping(i - 4, depth)

if __name__ == "__main__":
  rounds     = 200000
  class_time  = 0
  class_start = time.clock()
  for round in range(0, rounds):
    ex = Exception()
  class_time = time.clock() - class_start
  print("%d ex = Exception()'s %f" % (rounds, class_time))

  for depth in range(0, max_depth):
    #print("Depth %d" % depth)
    for round in range(0, rounds):
      catcher(depth, depth)

  for rep in range(0, max_depth):
    print("depth=%d time=%f" % (rep, time_total[rep]/1000000))

The output is, time (times are relative) take to call Exception()

200000 ex = Exception()'s 0.040469

depth=0 time=0.103843
depth=1 time=0.246050
depth=2 time=0.401459
depth=3 time=0.565742
depth=4 time=0.736362
depth=5 time=0.921993
depth=6 time=1.102257
depth=7 time=1.278089
depth=8 time=1.463500
depth=9 time=1.657082

Someone better at Python than me might be able to get py.test to print the timings at the end.

Note, There was a very similar question to this asked about Java a few weeks ago. It's a very informative thread regardless of language used...

Which part of throwing an Exception is expensive?

like image 132
Harry Avatar answered Oct 23 '22 21:10

Harry


In addition to Francesco's answer, it seems that one of the (relatively) expensive part of the catch is the exception matching:

>>> timeit.timeit('try:\n    raise KeyError\nexcept KeyError:\n    pass', number=1000000 )
1.1587663322268327
>>> timeit.timeit('try:\n    raise KeyError\nexcept:\n    pass', number=1000000 )
0.9180641582179874

Looking at the (CPython 2) disassembly:

>>> def f():
...     try:
...         raise KeyError
...     except KeyError:
...         pass
... 
>>> def g():
...     try:
...         raise KeyError
...     except:
...         pass
... 
>>> dis.dis(f)
  2           0 SETUP_EXCEPT            10 (to 13)

  3           3 LOAD_GLOBAL              0 (KeyError)
              6 RAISE_VARARGS            1
              9 POP_BLOCK           
             10 JUMP_FORWARD            17 (to 30)

  4     >>   13 DUP_TOP             
             14 LOAD_GLOBAL              0 (KeyError)
             17 COMPARE_OP              10 (exception match)
             20 POP_JUMP_IF_FALSE       29
             23 POP_TOP             
             24 POP_TOP             
             25 POP_TOP             

  5          26 JUMP_FORWARD             1 (to 30)
        >>   29 END_FINALLY         
        >>   30 LOAD_CONST               0 (None)
             33 RETURN_VALUE        
>>> dis.dis(g)
  2           0 SETUP_EXCEPT            10 (to 13)

  3           3 LOAD_GLOBAL              0 (KeyError)
              6 RAISE_VARARGS            1
              9 POP_BLOCK           
             10 JUMP_FORWARD             7 (to 20)

  4     >>   13 POP_TOP             
             14 POP_TOP             
             15 POP_TOP             

  5          16 JUMP_FORWARD             1 (to 20)
             19 END_FINALLY         
        >>   20 LOAD_CONST               0 (None)
             23 RETURN_VALUE        

Note that the catch block loads the Exception anyway and matches it against a KeyError. Indeed, looking at the except KeyError as ke case:

>>> def f2():
...     try:
...         raise KeyError
...     except KeyError as ke:
...         pass
... 
>>> dis.dis(f2)
  2           0 SETUP_EXCEPT            10 (to 13)

  3           3 LOAD_GLOBAL              0 (KeyError)
              6 RAISE_VARARGS            1
              9 POP_BLOCK           
             10 JUMP_FORWARD            19 (to 32)

  4     >>   13 DUP_TOP             
             14 LOAD_GLOBAL              0 (KeyError)
             17 COMPARE_OP              10 (exception match)
             20 POP_JUMP_IF_FALSE       31
             23 POP_TOP             
             24 STORE_FAST               0 (ke)
             27 POP_TOP             

  5          28 JUMP_FORWARD             1 (to 32)
        >>   31 END_FINALLY         
        >>   32 LOAD_CONST               0 (None)
             35 RETURN_VALUE    

The only difference is a single STORE_FAST to store the exception value (in case of a match). Similarly, having several exception matches:

>>> def f():
...     try:
...         raise ValueError
...     except KeyError:
...         pass
...     except IOError:
...         pass
...     except SomeOtherError:
...         pass
...     except:
...         pass
... 
>>> dis.dis(f)
  2           0 SETUP_EXCEPT            10 (to 13)

  3           3 LOAD_GLOBAL              0 (ValueError)
              6 RAISE_VARARGS            1
              9 POP_BLOCK           
             10 JUMP_FORWARD            55 (to 68)

  4     >>   13 DUP_TOP             
             14 LOAD_GLOBAL              1 (KeyError)
             17 COMPARE_OP              10 (exception match)
             20 POP_JUMP_IF_FALSE       29
             23 POP_TOP             
             24 POP_TOP             
             25 POP_TOP             

  5          26 JUMP_FORWARD            39 (to 68)

  6     >>   29 DUP_TOP             
             30 LOAD_GLOBAL              2 (IOError)
             33 COMPARE_OP              10 (exception match)
             36 POP_JUMP_IF_FALSE       45
             39 POP_TOP             
             40 POP_TOP             
             41 POP_TOP             

  7          42 JUMP_FORWARD            23 (to 68)

  8     >>   45 DUP_TOP             
             46 LOAD_GLOBAL              3 (SomeOtherError)
             49 COMPARE_OP              10 (exception match)
             52 POP_JUMP_IF_FALSE       61
             55 POP_TOP             
             56 POP_TOP             
             57 POP_TOP             

  9          58 JUMP_FORWARD             7 (to 68)

 10     >>   61 POP_TOP             
             62 POP_TOP             
             63 POP_TOP             

 11          64 JUMP_FORWARD             1 (to 68)
             67 END_FINALLY         
        >>   68 LOAD_CONST               0 (None)
             71 RETURN_VALUE      

Will duplicate the exception and try to match it against every exception listed, one by one until it founds a match, which is (probably) what is being hinted at as 'poor catch performance'.

like image 13
val Avatar answered Oct 23 '22 22:10

val


I think the two are the same in terms of speed:

>>> timeit.timeit('try:\n    raise KeyError\nexcept KeyError:\n    pass', number=1000000 )
0.7168641227143269
>>> timeit.timeit('try:\n    raise KeyError\nexcept KeyError as e:\n    pass', number=1000000 )
0.7733279216613766
like image 6
Francesco Avatar answered Oct 23 '22 20:10

Francesco