Use PyObject_Vectorcall for calls to Python for better efficiency

No longer requires creating temporary tuple to hold the args.
Introduced in Python 3.8.
This commit is contained in:
rdb 2026-01-07 20:09:20 +01:00
parent 8fe8862f06
commit 5a82ac3208
9 changed files with 112 additions and 89 deletions

View File

@ -332,8 +332,7 @@ add_done_callback(PyObject *self, PyObject *fn) {
}
PythonTask *task = new PythonTask(fn);
Py_DECREF(task->_args);
task->_args = PyTuple_Pack(1, self);
task->_args.assign(1, Py_NewRef(self));
task->_append_task = false;
task->_ignore_return = true;

View File

@ -37,7 +37,6 @@ PythonTask::
PythonTask(PyObject *func_or_coro, const std::string &name) :
AsyncTask(name),
_function(nullptr),
_args(nullptr),
_upon_death(nullptr),
_owner(nullptr),
_exception(nullptr),
@ -45,6 +44,7 @@ PythonTask(PyObject *func_or_coro, const std::string &name) :
_exc_traceback(nullptr),
_generator(nullptr),
_fut_waiter(nullptr),
_append_task(true),
_ignore_return(false),
_registered_to_owner(false),
_retrieved_exception(false) {
@ -63,7 +63,6 @@ PythonTask(PyObject *func_or_coro, const std::string &name) :
nassert_raise("Invalid function passed to PythonTask");
}
set_args(Py_None, true);
set_upon_death(Py_None);
set_owner(Py_None);
@ -104,7 +103,9 @@ PythonTask::
// All of these may have already been cleared by __clear__.
Py_XDECREF(_function);
Py_XDECREF(_args);
for (PyObject *arg : _args) {
Py_DECREF(arg);
}
Py_XDECREF(__dict__);
Py_XDECREF(_exception);
Py_XDECREF(_exc_value);
@ -134,21 +135,23 @@ set_function(PyObject *function) {
*/
void PythonTask::
set_args(PyObject *args, bool append_task) {
Py_XDECREF(_args);
_args = nullptr;
pvector<PyObject *> old_args = std::exchange(_args, {});
if (args == Py_None) {
// None means no arguments; create an empty tuple.
_args = PyTuple_New(0);
} else {
if (PySequence_Check(args)) {
_args = PySequence_Tuple(args);
if (PySequence_Check(args)) {
Py_ssize_t len = PySequence_Size(args);
_args.resize(len);
for (Py_ssize_t i = 0; i < len; ++i) {
_args[i] = PySequence_GetItem(args, i);
}
}
if (_args == nullptr) {
else if (args != Py_None) {
nassert_raise("Invalid args passed to PythonTask");
_args = PyTuple_New(0);
return;
}
for (PyObject *old_arg : old_args) {
Py_DECREF(old_arg);
}
_append_task = append_task;
@ -159,19 +162,18 @@ set_args(PyObject *args, bool append_task) {
*/
PyObject *PythonTask::
get_args() {
// If we want to append the task, we have to create a new tuple with space
// for one more at the end. We have to do this dynamically each time, to
// avoid storing the task itself in its own arguments list, and thereby
// creating a cyclical reference.
size_t num_args = _args.size();
PyObject *result = PyTuple_New(num_args + _append_task);
for (size_t i = 0; i < num_args; ++i) {
PyTuple_SET_ITEM(result, i, Py_NewRef(_args[i]));
}
if (_append_task) {
// If we want to append the task, we have to create a new tuple with space
// for one more at the end. We have to do this dynamically each time, to
// avoid storing the task itself in its own arguments list, and thereby
// creating a cyclical reference.
int num_args = PyTuple_GET_SIZE(_args);
PyObject *with_task = PyTuple_New(num_args + 1);
for (int i = 0; i < num_args; ++i) {
PyObject *item = PyTuple_GET_ITEM(_args, i);
PyTuple_SET_ITEM(with_task, i, Py_NewRef(item));
}
// Check whether we have a Python wrapper. This is not the case if the
// object has been created by C++ and never been exposed to Python code.
if (__self__ == nullptr) {
@ -180,12 +182,9 @@ get_args() {
__self__ = DTool_CreatePyInstance(this, Dtool_PythonTask, true, false);
}
PyTuple_SET_ITEM(with_task, num_args, Py_NewRef(__self__));
return with_task;
}
else {
return Py_NewRef(_args);
PyTuple_SET_ITEM(result, num_args, Py_NewRef(__self__));
}
return result;
}
/**
@ -376,7 +375,9 @@ int PythonTask::
__traverse__(visitproc visit, void *arg) {
Py_VISIT(__self__);
Py_VISIT(_function);
Py_VISIT(_args);
for (PyObject *arg : _args) {
Py_VISIT(arg);
}
Py_VISIT(_upon_death);
Py_VISIT(_owner);
Py_VISIT(__dict__);
@ -390,7 +391,12 @@ __traverse__(visitproc visit, void *arg) {
int PythonTask::
__clear__() {
Py_CLEAR(_function);
Py_CLEAR(_args);
{
pvector<PyObject *> old_args = std::exchange(_args, {});
for (PyObject *arg : old_args) {
Py_DECREF(arg);
}
}
Py_CLEAR(_upon_death);
Py_CLEAR(_owner);
Py_CLEAR(__dict__);
@ -590,9 +596,21 @@ do_python_task() {
// We are calling the function directly.
nassertr(_function != nullptr, DS_interrupt);
PyObject *args = get_args();
result = PythonThread::call_python_func(_function, args);
Py_DECREF(args);
size_t nargs = _args.size();
PyObject **args = (PyObject **)alloca(sizeof(PyObject *) * (nargs + 2)) + 1;
std::copy(_args.begin(), _args.end(), args);
if (_append_task) {
// Check whether we have a Python wrapper. This is not the case if the
// object has been created by C++ and never been exposed to Python code.
if (__self__ == nullptr) {
// A __self__ instance does not exist, let's create one now.
ref();
__self__ = DTool_CreatePyInstance(this, Dtool_PythonTask, true, false);
}
args[nargs++] = __self__;
}
result = PythonThread::call_python_func(_function, args, nargs | PY_VECTORCALL_ARGUMENTS_OFFSET);
if (result != nullptr && PyGen_Check(result)) {
// The function has yielded a generator. We will call into that

View File

@ -114,7 +114,7 @@ private:
private:
PyObject *_function;
PyObject *_args;
pvector<PyObject *> _args;
PyObject *_upon_death;
PyObject *_owner;

View File

@ -101,17 +101,19 @@ pre_load(const Filename &orig_filename, const Filename &orig_alpha_filename,
#endif
// Wrap the arguments.
PyObject *args = Py_BuildValue("(OOiiNO)",
DTool_CreatePyInstance((void *)&orig_filename, Dtool_Filename, false, true),
DTool_CreatePyInstance((void *)&orig_alpha_filename, Dtool_Filename, false, true),
primary_file_num_channels,
alpha_file_channel,
PyBool_FromLong(read_mipmaps),
DTool_CreatePyInstance((void *)&options, Dtool_LoaderOptions, false, true)
);
PyObject *args[7];
args[1] = DTool_CreatePyInstance((void *)&orig_filename, Dtool_Filename, false, true);
args[2] = DTool_CreatePyInstance((void *)&orig_alpha_filename, Dtool_Filename, false, true);
args[3] = PyLong_FromLong(primary_file_num_channels);
args[4] = PyLong_FromLong(alpha_file_channel);
args[5] = PyBool_FromLong(read_mipmaps);
args[6] = DTool_CreatePyInstance((void *)&options, Dtool_LoaderOptions, false, true);
PyObject *result = PythonThread::call_python_func(_pre_load_func, args);
Py_DECREF(args);
PyObject *result = PythonThread::call_python_func(_pre_load_func, args + 1, 6 | PY_VECTORCALL_ARGUMENTS_OFFSET);
for (size_t i = 1; i < 7; ++i) {
Py_DECREF(args[i]);
}
PT(Texture) tex;
if (result != nullptr) {
@ -162,11 +164,11 @@ post_load(Texture *tex) {
#endif
// Wrap the arguments.
PyObject *args = PyTuple_Pack(1,
DTool_CreatePyInstance(tex, Dtool_Texture, true, false)
);
PyObject *result = PythonThread::call_python_func(_post_load_func, args);
Py_DECREF(args);
PyObject *args[2];
args[1] = DTool_CreatePyInstance(tex, Dtool_Texture, true, false);
PyObject *result = PythonThread::call_python_func(_post_load_func, args + 1, 1 | PY_VECTORCALL_ARGUMENTS_OFFSET);
Py_DECREF(args[1]);
PT(Texture) result_tex;
if (result != nullptr) {

View File

@ -305,19 +305,19 @@ load_file(const Filename &path, const LoaderOptions &options,
#endif
// Wrap the arguments.
PyObject *args = PyTuple_New(3);
PyTuple_SET_ITEM(args, 0, DTool_CreatePyInstance((void *)&path, Dtool_Filename, false, true));
PyTuple_SET_ITEM(args, 1, DTool_CreatePyInstance((void *)&options, Dtool_LoaderOptions, false, true));
PyObject *args[4];
args[1] = DTool_CreatePyInstance((void *)&path, Dtool_Filename, false, true);
args[2] = DTool_CreatePyInstance((void *)&options, Dtool_LoaderOptions, false, true);
if (record != nullptr) {
record->ref();
PyTuple_SET_ITEM(args, 2, DTool_CreatePyInstanceTyped((void *)record, Dtool_BamCacheRecord, true, false, record->get_type_index()));
args[3] = DTool_CreatePyInstanceTyped((void *)record, Dtool_BamCacheRecord, true, false, record->get_type_index());
} else {
PyTuple_SET_ITEM(args, 2, Py_NewRef(Py_None));
args[3] = Py_NewRef(Py_None);
}
PT(PandaNode) node;
PyObject *result = PythonThread::call_python_func(_load_func, args);
PyObject *result = PythonThread::call_python_func(_load_func, args + 1, 3 | PY_VECTORCALL_ARGUMENTS_OFFSET);
if (result != nullptr) {
if (DtoolInstance_Check(result)) {
node = (PandaNode *)DtoolInstance_UPCAST(result, Dtool_PandaNode);
@ -325,7 +325,9 @@ load_file(const Filename &path, const LoaderOptions &options,
Py_DECREF(result);
}
Py_DECREF(args);
Py_DECREF(args[1]);
Py_DECREF(args[2]);
Py_DECREF(args[3]);
if (node == nullptr) {
PyObject *exc_type = PyErr_Occurred();
@ -372,13 +374,16 @@ save_file(const Filename &path, const LoaderOptions &options,
#endif
// Wrap the arguments.
PyObject *args = PyTuple_New(3);
PyTuple_SET_ITEM(args, 0, DTool_CreatePyInstance((void *)&path, Dtool_Filename, false, true));
PyTuple_SET_ITEM(args, 1, DTool_CreatePyInstance((void *)&options, Dtool_LoaderOptions, false, true));
PyTuple_SET_ITEM(args, 2, DTool_CreatePyInstanceTyped((void *)node, Dtool_PandaNode, true, false, node->get_type_index()));
PyObject *args[4];
args[1] = DTool_CreatePyInstance((void *)&path, Dtool_Filename, false, true);
args[2] = DTool_CreatePyInstance((void *)&options, Dtool_LoaderOptions, false, true);
args[3] = DTool_CreatePyInstanceTyped((void *)node, Dtool_PandaNode, true, false, node->get_type_index());
PyObject *result = PythonThread::call_python_func(_save_func, args + 1, 3 | PY_VECTORCALL_ARGUMENTS_OFFSET);
Py_DECREF(args[1]);
Py_DECREF(args[2]);
Py_DECREF(args[3]);
PyObject *result = PythonThread::call_python_func(_load_func, args);
Py_DECREF(args);
if (result != nullptr) {
Py_DECREF(result);
} else {

View File

@ -123,7 +123,7 @@ set_args(PyObject *args) {
* exception.
*/
PyObject *PythonThread::
call_python_func(PyObject *function, PyObject *args) {
call_python_func(PyObject *function, PyObject **args, size_t nargsf) {
Thread *current_thread = get_current_thread();
// Create a new Python thread state data structure, so Python can properly
@ -132,7 +132,7 @@ call_python_func(PyObject *function, PyObject *args) {
if (current_thread == get_main_thread()) {
// In the main thread, just call the function.
result = PyObject_Call(function, args, nullptr);
result = _PyObject_Vectorcall(function, args, nargsf, nullptr);
if (result == nullptr) {
if (PyErr_Occurred() && PyErr_ExceptionMatches(PyExc_SystemExit)) {
@ -189,7 +189,7 @@ call_python_func(PyObject *function, PyObject *args) {
PyThreadState_Swap(new_thread_state);
// Call the user's function.
result = PyObject_Call(function, args, nullptr);
result = _PyObject_Vectorcall(function, args, nargsf, nullptr);
if (result == nullptr && PyErr_Occurred()) {
// We got an exception. Move the exception from the current thread into
// the main thread, so it can be handled there.
@ -230,7 +230,7 @@ call_python_func(PyObject *function, PyObject *args) {
gstate = PyGILState_Ensure();
// Call the user's function.
result = PyObject_Call(function, args, nullptr);
result = _PyObject_Vectorcall(function, args, nargsf, nullptr);
if (result == nullptr && PyErr_Occurred()) {
// We got an exception. Move the exception from the current thread into
// the main thread, so it can be handled there.
@ -275,7 +275,9 @@ call_python_func(PyObject *function, PyObject *args) {
*/
void PythonThread::
thread_main() {
_result = call_python_func(_function, _args);
PyObject **args = &PyTuple_GET_ITEM(_args, 0);
Py_ssize_t nargs = PyTuple_GET_SIZE(_args);
_result = call_python_func(_function, args, (size_t)nargs);
}
#endif // HAVE_PYTHON

View File

@ -36,7 +36,8 @@ public:
PyObject *get_args() const;
void set_args(PyObject *);
static PyObject *call_python_func(PyObject *function, PyObject *args);
static PyObject *call_python_func(PyObject *function, PyObject **args,
size_t nargsf);
PUBLISHED:
MAKE_PROPERTY(args, get_args, set_args);

View File

@ -41,15 +41,14 @@ static TypedWritable *factory_callback(const FactoryParams &params){
BamReader *manager;
parse_params(params, scan, manager);
PyObject *py_scan = DTool_CreatePyInstance(&scan, Dtool_DatagramIterator, false, false);
PyObject *py_manager = DTool_CreatePyInstance(manager, Dtool_BamReader, false, false);
PyObject *args = PyTuple_Pack(2, py_scan, py_manager);
PyObject *args[3];
args[1] = DTool_CreatePyInstance(&scan, Dtool_DatagramIterator, false, false);
args[2] = DTool_CreatePyInstance(manager, Dtool_BamReader, false, false);
// Now call the Python function.
PyObject *result = PythonThread::call_python_func(func, args);
Py_DECREF(args);
Py_DECREF(py_scan);
Py_DECREF(py_manager);
PyObject *result = PythonThread::call_python_func(func, args + 1, 2 | PY_VECTORCALL_ARGUMENTS_OFFSET);
Py_DECREF(args[1]);
Py_DECREF(args[2]);
if (result == nullptr) {
util_cat.error()

View File

@ -129,16 +129,13 @@ void PythonCallbackObject::
do_python_callback(CallbackData *cbdata) {
nassertv(cbdata != nullptr);
// Wrap the cbdata up in a Python object, then put it in a tuple, for the
// argument list.
PyObject *pycbdata =
DTool_CreatePyInstanceTyped(cbdata, Dtool_TypedObject,
false, false, cbdata->get_type_index());
PyObject *args = Py_BuildValue("(O)", pycbdata);
Py_DECREF(pycbdata);
// Wrap the cbdata up in a Python object.
PyObject *args[2];
args[1] = DTool_CreatePyInstanceTyped(cbdata, Dtool_TypedObject,
false, false, cbdata->get_type_index());
PyObject *result = PythonThread::call_python_func(_function, args);
Py_DECREF(args);
PyObject *result = PythonThread::call_python_func(_function, args + 1, 1 | PY_VECTORCALL_ARGUMENTS_OFFSET);
Py_DECREF(args[1]);
if (result == nullptr) {
if (PyErr_Occurred() != PyExc_SystemExit) {