[Cython] Redundant Cython exception message strings

Stefan Behnel stefan_ml at behnel.de
Sat May 28 11:37:23 CEST 2011


Robert Bradshaw, 28.05.2011 00:39:
> On Fri, May 27, 2011 at 3:32 PM, Stefan Behnel wrote:
>> I recently stumbled over a tradeoff question with AttributeError, and now
>> found the same situation for UnboundLocalError in Vitja's control flow
>> branch. So here it is.
>>
>> When we raise an exception several times in different parts of the code with
>> a message that only differs slightly each time (usually something like
>> "'NoneType' has no attribute X", or "local variable X referenced before
>> assignment"), we have three choices to handle this:
>>
>> 1) Optimise for speed: create a Python string object at module
>> initialisation time and call PyErr_SetObject(exc_type, msg_str_obj).
>>
>> 2) Current way: let CPython create the string object when raising the
>> exception and just call PyErr_SetString(exc_type, "complete message").
>>
>> 3) Trade speed for size and allow the C compiler to reduce the storage
>> redundancy: write only the message template and the names as C char*
>> constants by calling PyErr_Format(exc_type, "message template %s", "X").
>>
>> Assuming that exceptions should be exceptional, I'm leaning towards 3). This
>> would allow the C compiler to collapse multiple usages of the same C string
>> into one data constant, thus reducing a bit of redundancy in the shared
>> library size and the memory footprint. However, it would (slightly?) slow
>> down the exception raising due to the additional string formatting, even
>> when compared to the need to build a Python string object that it shares
>> with 2). While 1) would obviously be the fastest way to raise an exception
>> (no memory allocation, only refcounting), I think it's not worth it for
>> exceptions as it increases both the runtime memory overhead and the module
>> startup time.
>
> Any back-of-the-envelope calculations on how much the savings would
> be?

As a micro benchmark, I wrote three C functions that do 10 exception 
setting calls and then clear the exception, and called those 10x in a loop 
(i.e. 100 exceptions). Results:

1) PyErr_SetObject(PyExc_TypeError, Py_None)
Py3.3: 1000000 loops, best of 3: 1.42 usec
Py2.7: 1000000 loops, best of 3: 0.965 usec

2) PyErr_SetString(PyExc_TypeError, "[complete message]")
Py3.3: 100000 loops, best of 3: 11.2 usec
Py2.7: 100000 loops, best of 3: 4.85 usec

3) PyErr_Format(PyExc_TypeError, "[message %s template]", "Abc1")
Py3.3: 10000 loops, best of 3: 37.3 usec
Py2.7: 10000 loops, best of 3: 25.3 usec

Observations: these are really tiny numbers for 100 exceptions. The string 
formatting case is only some 0.3 microseconds (25x) slower per exception 
than the constant pointer case, and about 0.2 microseconds (4-5x) slower 
than the C string constant case.

Note that this only benchmarks the exception setting, not the catching, 
i.e. without the instantiation of the exception object etc., which is 
identical for all three cases.

This change would only apply to Cython generated exceptions (from None 
safety checks, unbound locals, etc.), which can appear in a lot of places 
in the C code but should not normally be triggered in production code. If 
they occur, we'd loose about 0.2 microseconds per exception, comparing 2) 
and 3). I think that's totally negligible, given that these exceptions 
potentially indicate a bug in the user code.

"strings" tells me that the C compiler really only keeps one copy of the 
string constants. The savings per exception message are somewhere between 
30 and 40 bytes. Not much in today's categories. Assuming even 1000 such 
exceptions in a large module, that's only some 30K of savings, whereas such 
a module would likely have a total stripped size of a *lot* more than 1MB.

Personally, I think that the performance degradation is basically 
non-existent, so the space savings come almost for free, however tiny they 
may be.

Stefan


More information about the cython-devel mailing list