In last post, I have mentioned the tools which I used for profiling numpy. Call-graph, made my life not easier but a bit simpler to detect time consuming methods. But time taken just indicate the possibility of bottlenecks as many of time consuming methods are highly optimized. Hence, critically reading code along with call-graph is the trick
Identifying bottlenecks
Following types bottlenecks were observed
Overhead for smaller arrays
- Numpy used to release Global Interpreter Lock or GIL for all two or one operand loops. But for short length array, it used to produce relative overhead instead.
- So, releasing GIL for smaller operations was not benefiting at all.
Redundant code doing extra work
PyArrayCanCastArrayToused to check evenif newtype is Null. InPyArrayFromArray, It was found that if argument newtype is Null, it get value of oldtype. So, technically it has to check if casting is possible for same type, which is useless.- Numpy used to converts the Python scalar into its matching scalar (e.g. PyLong -> int32) and then extract the C value from the NumPy scalar.
- Clearing the error flags at every function call, then checking it. This happens unconditional even-if there is no need to do.
Improper structure
- For every single operation calls, numpy has to extract value of buffersize, errormask and name to pack and build error object. These two functions, _extract_pyvals and PyUFunc_GetPyValues together use significant time. It take useless time because all this time is spent on to look up entries in a python dict, extract them, and convert them into C level data. Not once but doing that again and again on every operation. Also which remain unused if no error occurs.
- loop selection method for scalar operation is inefficient and consume time. It check through all associated dtypes of function one by one from types array.
Not so effective memory allocation
- When allocating the return value, numpy allocate memory twice. One for the array object itself, and a second time for the array data.
- Also, shapes + strides together have 2*ndim elements, but to hold them numpy allocate a memory region sized to hold 3*ndim elements.


