Hi, 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. Thoughts? Stefan
On Fri, May 27, 2011 at 3:32 PM, Stefan Behnel <stefan_ml@behnel.de> wrote:
Hi,
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.
Thoughts?
Any back-of-the-envelope calculations on how much the savings would be? I think I'm leaning towards 3 as well, certainly not option 1. - Robert
2011/5/28 Robert Bradshaw <robertwb@math.washington.edu>:
On Fri, May 27, 2011 at 3:32 PM, Stefan Behnel <stefan_ml@behnel.de> wrote:
Hi,
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.
Thoughts?
Any back-of-the-envelope calculations on how much the savings would be? I think I'm leaning towards 3 as well, certainly not option 1.
For UnboundLocalError and NameError I used 2) way: https://github.com/vitek/cython/commit/1fe86b85d965753244cd09db38b1089b40f09... So maybe I should add functions like __Pyx_RaiseUnboundLocalError and __Pyx_RaiseClosureNameError that will use 3) way. How do you like put_error_if_unbound CCodeWriter method is that right place for it? -- vitja.
On Sat, May 28, 2011 at 1:15 AM, Vitja Makarov <vitja.makarov@gmail.com> wrote:
2011/5/28 Robert Bradshaw <robertwb@math.washington.edu>:
On Fri, May 27, 2011 at 3:32 PM, Stefan Behnel <stefan_ml@behnel.de> wrote:
Hi,
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.
Thoughts?
Any back-of-the-envelope calculations on how much the savings would be? I think I'm leaning towards 3 as well, certainly not option 1.
For UnboundLocalError and NameError I used 2) way:
https://github.com/vitek/cython/commit/1fe86b85d965753244cd09db38b1089b40f09...
So maybe I should add functions like __Pyx_RaiseUnboundLocalError and __Pyx_RaiseClosureNameError that will use 3) way. How do you like put_error_if_unbound CCodeWriter method is that right place for it?
I don't think abstracting it out to a function really saves anything here given that Python already has PyErr_Format. - Robert
Robert Bradshaw, 28.05.2011 18:14:
On Sat, May 28, 2011 at 1:15 AM, Vitja Makarov wrote:
So maybe I should add functions like __Pyx_RaiseUnboundLocalError and __Pyx_RaiseClosureNameError that will use 3) way. How do you like put_error_if_unbound CCodeWriter method is that right place for it?
I don't think abstracting it out to a function really saves anything here given that Python already has PyErr_Format.
Agreed. The existing functions are only there because they are used in multiple places, and usually receive formatting arguments. So putting them into one function makes it easier to keep the message consistent etc. I also checked that even if you only call PyErr_Format() with multiple constant string arguments, without going through a function that explicitly declares them "const char*", gcc considers them "const char*" automatically and reuses the same constant in different places. Stefan
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
On Sat, May 28, 2011 at 2:37 AM, Stefan Behnel <stefan_ml@behnel.de> wrote:
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.
Sounds good. I'm fine with 2 or 3, and despite the performance advantage of 1, it should be the exceptional case to raise this kind of error, and the module initialization time is and issue. - Robert
participants (3)
-
Robert Bradshaw -
Stefan Behnel -
Vitja Makarov