Skip to content

Commit c6cd07f

Browse files
eendebakptclaude
andcommitted
Speed up zero-argument class patterns in match statements
A class pattern with no sub-patterns (`case C():`) is just an isinstance check, but it compiled to LOAD_COMMON_CONSTANT (empty names tuple) + MATCH_CLASS + COPY + POP_JUMP_IF_NONE + UNPACK_SEQUENCE 0, building and unpacking an empty attributes tuple every time. Compile it instead to a CALL_INTRINSIC_2 invoking a new INTRINSIC_MATCH_CLASS_ISINSTANCE, which performs the same PyType_Check and PyObject_IsInstance as _PyEval_MatchClass (including the identical TypeError when the pattern does not name a class) and pushes a bool. No new opcode is added. This is ~25% faster on a matching subject and ~14% faster on a non-matching subject for the zero-argument case, and is even faster than an equivalent isinstance() call since the intrinsic avoids a Python-level call frame. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 1b0c05f commit c6cd07f

3 files changed

Lines changed: 32 additions & 1 deletion

File tree

Include/internal/pycore_intrinsics.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@
3030
#define INTRINSIC_TYPEVAR_WITH_CONSTRAINTS 3
3131
#define INTRINSIC_SET_FUNCTION_TYPE_PARAMS 4
3232
#define INTRINSIC_SET_TYPEPARAM_DEFAULT 5
33+
#define INTRINSIC_MATCH_CLASS_ISINSTANCE 6
3334

34-
#define MAX_INTRINSIC_2 5
35+
#define MAX_INTRINSIC_2 6
3536

3637
typedef PyObject *(*intrinsic_func1)(PyThreadState* tstate, PyObject *value);
3738
typedef PyObject *(*intrinsic_func2)(PyThreadState* tstate, PyObject *value1, PyObject *value2);

Python/codegen.c

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6180,6 +6180,17 @@ codegen_pattern_class(compiler *c, pattern_ty p, pattern_context *pc)
61806180
PyObject *name = asdl_seq_GET(kwd_attrs, i);
61816181
PyTuple_SET_ITEM(attr_names, i, Py_NewRef(name));
61826182
}
6183+
if (nargs + nattrs == 0) {
6184+
// No sub-patterns (`case C():`). This is just an isinstance check.
6185+
// Emit it as a CALL_INTRINSIC_2 instead of MATCH_CLASS so we avoid
6186+
// loading the empty names tuple, building the (empty) attrs tuple and
6187+
// unpacking it. The class is already on top of the subject. The
6188+
// intrinsic consumes both and pushes a bool.
6189+
Py_DECREF(attr_names);
6190+
ADDOP_I(c, LOC(p), CALL_INTRINSIC_2, INTRINSIC_MATCH_CLASS_ISINSTANCE);
6191+
RETURN_IF_ERROR(jump_to_fail_pop(c, LOC(p), pc, POP_JUMP_IF_FALSE));
6192+
return SUCCESS;
6193+
}
61836194
ADDOP_LOAD_CONST_NEW(c, LOC(p), attr_names);
61846195
ADDOP_I(c, LOC(p), MATCH_CLASS, nargs);
61856196
ADDOP_I(c, LOC(p), COPY, 1);

Python/intrinsics.c

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,24 @@ prep_reraise_star(PyThreadState* unused, PyObject *orig, PyObject *excs)
254254
return _PyExc_PrepReraiseStar(orig, excs);
255255
}
256256

257+
static PyObject *
258+
match_class_isinstance(PyThreadState* tstate, PyObject *subject, PyObject *type)
259+
{
260+
// Fast path for a class pattern with no sub-patterns (`case C():`).
261+
// Equivalent to the isinstance check performed by _PyEval_MatchClass,
262+
// including the same TypeError when the pattern does not name a class.
263+
if (!PyType_Check(type)) {
264+
_PyErr_SetString(tstate, PyExc_TypeError,
265+
"class pattern must refer to a class");
266+
return NULL;
267+
}
268+
int res = PyObject_IsInstance(subject, type);
269+
if (res < 0) {
270+
return NULL;
271+
}
272+
return PyBool_FromLong(res);
273+
}
274+
257275
static PyObject *
258276
make_typevar_with_bound(PyThreadState* Py_UNUSED(ignored), PyObject *name,
259277
PyObject *evaluate_bound)
@@ -278,6 +296,7 @@ _PyIntrinsics_BinaryFunctions[] = {
278296
INTRINSIC_FUNC_ENTRY(INTRINSIC_TYPEVAR_WITH_CONSTRAINTS, make_typevar_with_constraints)
279297
INTRINSIC_FUNC_ENTRY(INTRINSIC_SET_FUNCTION_TYPE_PARAMS, _Py_set_function_type_params)
280298
INTRINSIC_FUNC_ENTRY(INTRINSIC_SET_TYPEPARAM_DEFAULT, _Py_set_typeparam_default)
299+
INTRINSIC_FUNC_ENTRY(INTRINSIC_MATCH_CLASS_ISINSTANCE, match_class_isinstance)
281300
};
282301

283302
#undef INTRINSIC_FUNC_ENTRY

0 commit comments

Comments
 (0)