PERF, BUG: use faster calling conventions (vectorcall) in several hot paths#31988
PERF, BUG: use faster calling conventions (vectorcall) in several hot paths#31988eendebakpt wants to merge 2 commits into
Conversation
Replace PyObject_CallMethod/PyObject_CallFunction with format strings and per-call attribute-name creation by the faster vectorcall based calling conventions (PyObject_Vectorcall, PyObject_CallOneArg, PyObject_CallMethodNoArgs/OneArg, PyObject_CallNoArgs) and interned strings: - PyUFunc_On_Om (np.frompyfunc / np.vectorize inner loop): vectorcall with a stack array instead of allocating an argument tuple per element - PyUFunc_O_O_method / PyUFunc_OO_O_method: create the method name once per loop instead of once per element - object loops of floor/ceil/trunc/gcd: PyObject_CallOneArg / vectorcall - object matmul and vdot: interned 'conjugate' + PyObject_CallMethodNoArgs - get_tzoffset_from_pytzinfo: interned 'astimezone' + CallMethodOneArg - PyArray_CopyConverter: interned 'value' attribute lookup - number.c generic (inplace) binary/unary forwarding: vectorcall - PyArray_Ptp/Round/Std and _void_compare: vectorcall instead of format-string PyObject_CallFunction Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
I think this PR introduces a borrowed-reference lifetime bug in the object
Reproducer on this branch: import faulthandler
import numpy as np
faulthandler.enable()
victim_array = None
class Killer:
def __index__(self):
global victim_array
print("Killer.__index__: clearing victim array slot", flush=True)
victim_array[0] = None
print("Killer.__index__: cleared", flush=True)
return 12
class Victim:
def __index__(self):
print("Victim.__index__", flush=True)
return 8
def __del__(self):
print("Victim.__del__", flush=True)
left = np.empty(1, dtype=object)
left[0] = Killer()
victim_array = np.empty(1, dtype=object)
victim_array[0] = Victim()
print("about to call np.gcd", flush=True)
np.gcd(left, victim_array)I ran this in a subprocess locally and got As a control, keeping an extra Python reference to Suggested fix: keep strong refs around the vectorcall, at least for |
The vectorcall-based calls receive borrowed references from the object array slots; re-entrant code run during the call (e.g. __index__) can clear those slots and free an operand mid-call. Hold strong references in npy_ObjectGCD, npy_ObjectLCM and PyUFunc_OO_O_method. Also fix a pre-existing use-after-free in the PyUFunc_O_O_method error path, which read Py_TYPE(in1) after the attribute lookup may have freed in1 (reproducible on main and 2.5.1 with PYTHONMALLOC=debug). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
I agree. Similar bugs are already present on main, I am addressing those as well. |
PR summary
Replace
PyObject_CallMethod/PyObject_CallFunctionby the faster vectorcall based calling conventions (PyObject_Vectorcall,PyObject_CallOneArg,PyObject_CallMethodNoArgs/OneArg,PyObject_CallNoArgs) and interned strings. This saves temporary tuple creation, variadic argument parsing and repeated creation of temporary strings.Benchmark results for a few selected cases (pyperf, Windows, MSVC):
Benchmark script
AI Disclosure
Claude was prompted to find all inefficient python calls in a performance critical path (this means there are some cases remaining that can be converted if needed). The benchmark script was written by Claude as well.