diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-09-15-13-28-48.gh-issue-138912.61EYbn.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-09-15-13-28-48.gh-issue-138912.61EYbn.rst new file mode 100644 index 00000000000000..f5d312a289fe21 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-09-15-13-28-48.gh-issue-138912.61EYbn.rst @@ -0,0 +1 @@ +Improve :opcode:`MATCH_CLASS` performance by up to 52% in certain cases. Patch by Marc Mueller. diff --git a/Python/ceval.c b/Python/ceval.c index 578c5d2a8b1420..eb618b5e4f8654 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -709,15 +709,18 @@ match_class_attr(PyThreadState *tstate, PyObject *subject, PyObject *type, PyObject *name, PyObject *seen) { assert(PyUnicode_CheckExact(name)); - assert(PySet_CheckExact(seen)); - if (PySet_Contains(seen, name) || PySet_Add(seen, name)) { - if (!_PyErr_Occurred(tstate)) { - // Seen it before! - _PyErr_Format(tstate, PyExc_TypeError, - "%s() got multiple sub-patterns for attribute %R", - ((PyTypeObject*)type)->tp_name, name); + // Only check for duplicates if seen is not NULL. + if (seen != NULL) { + assert(PySet_CheckExact(seen)); + if (PySet_Contains(seen, name) || PySet_Add(seen, name)) { + if (!_PyErr_Occurred(tstate)) { + // Seen it before! + _PyErr_Format(tstate, PyExc_TypeError, + "%s() got multiple sub-patterns for attribute %R", + ((PyTypeObject*)type)->tp_name, name); + } + return NULL; } - return NULL; } PyObject *attr; (void)PyObject_GetOptionalAttr(subject, name, &attr); @@ -740,14 +743,25 @@ _PyEval_MatchClass(PyThreadState *tstate, PyObject *subject, PyObject *type, if (PyObject_IsInstance(subject, type) <= 0) { return NULL; } + // Short circuit if there aren't any arguments: + Py_ssize_t nkwargs = PyTuple_GET_SIZE(kwargs); + Py_ssize_t nattrs = nargs + nkwargs; + if (!nattrs) { + return PyTuple_New(0); + } // So far so good: - PyObject *seen = PySet_New(NULL); - if (seen == NULL) { - return NULL; + PyObject *seen = NULL; + // Only check for duplicates if there is at least one positional attribute + // and two or more attributes in total. + if (nargs > 0 && nattrs > 1) { + seen = PySet_New(NULL); + if (seen == NULL) { + return NULL; + } } - PyObject *attrs = PyList_New(0); + PyObject *attrs = PyTuple_New(nattrs); if (attrs == NULL) { - Py_DECREF(seen); + Py_XDECREF(seen); return NULL; } // NOTE: From this point on, goto fail on failure: @@ -788,9 +802,9 @@ _PyEval_MatchClass(PyThreadState *tstate, PyObject *subject, PyObject *type, } if (match_self) { // Easy. Copy the subject itself, and move on to kwargs. - if (PyList_Append(attrs, subject) < 0) { - goto fail; - } + Py_NewRef(subject); + assert(PyTuple_GET_ITEM(attrs, 0) == NULL); + PyTuple_SET_ITEM(attrs, 0, subject); } else { for (Py_ssize_t i = 0; i < nargs; i++) { @@ -806,36 +820,29 @@ _PyEval_MatchClass(PyThreadState *tstate, PyObject *subject, PyObject *type, if (attr == NULL) { goto fail; } - if (PyList_Append(attrs, attr) < 0) { - Py_DECREF(attr); - goto fail; - } - Py_DECREF(attr); + assert(PyTuple_GET_ITEM(attrs, i) == NULL); + PyTuple_SET_ITEM(attrs, i, attr); } } Py_CLEAR(match_args); } // Finally, the keyword subpatterns: - for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(kwargs); i++) { + for (Py_ssize_t i = 0; i < nkwargs; i++) { PyObject *name = PyTuple_GET_ITEM(kwargs, i); PyObject *attr = match_class_attr(tstate, subject, type, name, seen); if (attr == NULL) { goto fail; } - if (PyList_Append(attrs, attr) < 0) { - Py_DECREF(attr); - goto fail; - } - Py_DECREF(attr); + assert(PyTuple_GET_ITEM(attrs, nargs + i) == NULL); + PyTuple_SET_ITEM(attrs, nargs + i, attr); } - Py_SETREF(attrs, PyList_AsTuple(attrs)); - Py_DECREF(seen); + Py_XDECREF(seen); return attrs; fail: // We really don't care whether an error was raised or not... that's our // caller's problem. All we know is that the match failed. Py_XDECREF(match_args); - Py_DECREF(seen); + Py_XDECREF(seen); Py_DECREF(attrs); return NULL; }