Skip to content

Make sure that tp_as_xxx methods are never NULL in PyType_Ready() - #157499

Closed
vstinner wants to merge 2 commits into
python:mainfrom
vstinner:null_methods
Closed

vstinner wants to merge 2 commits into
python:mainfrom
vstinner:null_methods

Conversation

@vstinner

Copy link
Copy Markdown
Member

PyType_Ready() now sets type tp_as_xxx members to a structure filled of NULL if a member is NULL. It avoids checking if tp_as_xxx is NULL in Objects/abstract.c functions.

PyType_Ready() now sets type tp_as_xxx members to a structure filled
of NULL if a member is NULL. It avoids checking if tp_as_xxx is NULL
in Objects/abstract.c functions.
@vstinner vstinner added the performance Performance or resource usage label Sep 14, 2026
@vstinner

Copy link
Copy Markdown
Member Author

I wrote a patch to run a microbenchmark on PyNumber_Add() with two integers (1+2):

Details
diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c
index eb769294fd2..c388711fdf1 100644
--- a/Modules/_testcapimodule.c
+++ b/Modules/_testcapimodule.c
@@ -2879,6 +2879,47 @@ uptime_bsd(PyObject *Py_UNUSED(self), PyObject *Py_UNUSED(args))
 #endif
 
 
+static PyObject*
+bench_add(PyObject *Py_UNUSED(self), PyObject *args)
+{
+    Py_ssize_t loops;
+    if (!PyArg_ParseTuple(args, "n", &loops)) {
+        return NULL;
+    }
+
+    PyObject *one = PyLong_FromLong(1);
+    PyObject *two = PyLong_FromLong(1);
+    assert(one != NULL && two != NULL);
+
+    PyTime_t t1, t2;
+    (void)PyTime_PerfCounterRaw(&t1);
+    PyObject *sum;
+
+#define ADD() \
+        sum = PyNumber_Add(one, two); \
+        if (sum == NULL) { \
+            return NULL; \
+        } \
+        Py_DECREF(sum);
+
+    for (Py_ssize_t i=0; i < loops; i++) {
+        ADD();
+        ADD();
+        ADD();
+        ADD();
+        ADD();
+        ADD();
+        ADD();
+        ADD();
+        ADD();
+        ADD();
+    }
+
+    (void)PyTime_PerfCounterRaw(&t2);
+    return PyFloat_FromDouble(PyTime_AsSecondsDouble(t2 - t1));
+}
+
+
 static PyMethodDef TestMethods[] = {
     {"set_errno",               set_errno,                       METH_VARARGS},
     {"test_config",             test_config,                     METH_NOARGS},
@@ -2978,6 +3019,7 @@ static PyMethodDef TestMethods[] = {
 #ifdef HAVE_SYSCTLBYNAME
     {"uptime_bsd", uptime_bsd, METH_NOARGS},
 #endif
+    {"bench_add", bench_add, METH_VARARGS},
     {NULL, NULL} /* sentinel */
 };
 

Benchmark:

import pyperf
import _testcapi
runner = pyperf.Runner()
runner.bench_time_func('int + int', _testcapi.bench_add)

Result: Mean +- std dev: [isol_ref] 88.1 ns +- 1.4 ns -> [isol_change] 87.4 ns +- 0.0 ns: 1.01x faster.

Well... the difference is quite minor. So I'm not sure that the change is worth it.

@vstinner

Copy link
Copy Markdown
Member Author

I also tried adding PyNumberMethods _as_number; to PyTypeObject and modify PyNumber_Add() to use it to avoid one pointer indirection. Again, there is no significant performance difference on PyNumber_Add(1, 1), so this change is not worth it. I prefer to close my PR.

This work would be easier to do if PyTypeObject structure was opaque, but it's part of the public C API, so it cannot be easily changed :-( We have to keep tp_as_number pointer (and check if it's not-NULL before using it, for now).

@vstinner vstinner closed this Sep 15, 2026
@vstinner
vstinner deleted the null_methods branch September 15, 2026 14:16
@ZeroIntensity

Copy link
Copy Markdown
Member

The performance impact might be better than you think! See #149180.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

performance Performance or resource usage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants