gh-138912: Improve MATCH_CLASS opcode performance (GH-138915)

Only check for duplicates if there is at least one positional pattern.
With a test case for duplicate keyword attributes.
This commit is contained in:
Marc Mueller 2026-02-14 06:06:15 +01:00 committed by GitHub
parent fdbc135f9c
commit 75d4839fa9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 45 additions and 31 deletions

View file

@ -509,15 +509,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);
@ -540,14 +543,26 @@ _PyEval_MatchClass(PyThreadState *tstate, PyObject *subject, PyObject *type,
if (PyObject_IsInstance(subject, type) <= 0) {
return NULL;
}
// So far so good:
PyObject *seen = PySet_New(NULL);
if (seen == NULL) {
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);
}
PyObject *attrs = PyList_New(0);
// So far so good:
PyObject *seen = NULL;
// Only check for duplicates if there is at least one positional attribute
// and two or more attributes in total. Duplicate keyword attributes are
// detected during the compile stage and raise a SyntaxError.
if (nargs > 0 && nattrs > 1) {
seen = PySet_New(NULL);
if (seen == NULL) {
return NULL;
}
}
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:
@ -588,9 +603,8 @@ _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;
}
assert(PyTuple_GET_ITEM(attrs, 0) == NULL);
PyTuple_SET_ITEM(attrs, 0, Py_NewRef(subject));
}
else {
for (Py_ssize_t i = 0; i < nargs; i++) {
@ -606,36 +620,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;
}