From 558865f5355bbf77e086a87c69d0e9a5981c831b Mon Sep 17 00:00:00 2001 From: Matthew Parkinson Date: Thu, 2 Apr 2026 10:40:38 +0100 Subject: [PATCH 1/3] Improve Py_CHECKWRITE codegen by moving cold checks out of line Replace the Py_CHECKWRITE macro with a static inline function that keeps only the _Py_IsImmutable flag test in the hot path. The _PyImmModule_Check (which expands to PyObject_TypeCheck and may call PyType_IsSubtype) and Py_IsFinalizing checks are moved into a new out-of-line function _Py_CheckWriteImmutable in Python/immutability.c. The NULL check is removed as callers never pass NULL. Before this change, every Py_CHECKWRITE call site inlined the full ImmModule type check into the hot path (load ob_type, compare to _PyImmModule_Type, conditional call to PyType_IsSubtype). After, the hot path is just testb + jne (not taken for mutable objects), with the cold path behind a single opaque function call. This is visible in the interpreter's STORE_ATTR_SLOT and in functions like set_pop_impl, set_add_key, dict mutation paths, etc. --- Include/refcount.h | 16 ++++++++++++++-- Python/immutability.c | 9 +++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/Include/refcount.h b/Include/refcount.h index 204e06cfacd7b0..3d8e9e1a8da1a0 100644 --- a/Include/refcount.h +++ b/Include/refcount.h @@ -142,10 +142,22 @@ static inline Py_ALWAYS_INLINE int _Py_IsImmutable(PyObject *op) } #define _Py_IsImmutable(op) _Py_IsImmutable(_PyObject_CAST(op)) +// Cold path for Py_CHECKWRITE: called when the object is immutable. +// Returns 1 if the object is still writable (ImmModule or finalizing). +PyAPI_FUNC(int) _Py_CheckWriteImmutable(PyObject *op); + // Artifact[Implementation]: The definition of the `Py_CHECKWRITE` macro // Check whether an object is writeable. -// This check will always succeed during runtime finalization. -#define Py_CHECKWRITE(op) ((op) && (!_Py_IsImmutable(op) || _PyImmModule_Check(op) || Py_IsFinalizing())) +// The fast path just checks the immutable flag; the ImmModule and +// finalizing checks are pushed into a cold out-of-line function. +static inline int Py_CHECKWRITE(PyObject *op) +{ + if (_Py_IsImmutable(op)) { + return _Py_CheckWriteImmutable(op); + } + return 1; +} +#define Py_CHECKWRITE(op) Py_CHECKWRITE(_PyObject_CAST(op)) #define Py_REQUIREWRITE(op, msg) {if (Py_CHECKWRITE(op)) { _PyObject_ASSERT_FAILED_MSG(op, msg); }} static inline Py_ALWAYS_INLINE void _Py_CLEAR_IMMUTABLE(PyObject *op) diff --git a/Python/immutability.c b/Python/immutability.c index c4feb45d0511c7..1f443ed11d3a4c 100644 --- a/Python/immutability.c +++ b/Python/immutability.c @@ -1846,6 +1846,15 @@ int _PyImmutability_CanViewAsImmutable(PyObject *obj) return 1; } +// Cold path for Py_CHECKWRITE: the object is known to be immutable. +// Returns 1 (writable) if the object's type is _PyImmModule_Type or +// the runtime is finalizing. +int +_Py_CheckWriteImmutable(PyObject *op) +{ + return _PyImmModule_Check(op) || Py_IsFinalizing(); +} + // Perform a decref on an immutable object // returns true if the object should be deallocated. int _Py_DecRef_Immutable(PyObject *op) From 0986998e7dce895fd186b24bbc597727ccabc57e Mon Sep 17 00:00:00 2001 From: Matthew Parkinson Date: Thu, 2 Apr 2026 10:51:22 +0100 Subject: [PATCH 2/3] Format issue from previous PR. --- Python/pystate.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python/pystate.c b/Python/pystate.c index 90ea2571941a56..32825515a063c1 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -1386,7 +1386,7 @@ PyModuleObject* _PyInterpreterState_GetModuleState(PyObject *mod) { if (modules_mod == mod) { // The modules in `sys.modules` is this frozen mod but there is // no mutable state in `sys.mut_modules`, probably because it was - // unimported? Either way: + // unimported? Either way: // Remove `mod` from `sys.modules` and trigger a reimport. if (PyDict_DelItem(modules, self->md_name) < 0) { Py_DECREF(modules_mod); From 0993614e8a8a05711393e780886dfc14cbc1064d Mon Sep 17 00:00:00 2001 From: Matthew Parkinson Date: Thu, 2 Apr 2026 11:24:07 +0100 Subject: [PATCH 3/3] Fixes for limited API --- Include/refcount.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Include/refcount.h b/Include/refcount.h index 3d8e9e1a8da1a0..b9769d80929ee0 100644 --- a/Include/refcount.h +++ b/Include/refcount.h @@ -142,11 +142,11 @@ static inline Py_ALWAYS_INLINE int _Py_IsImmutable(PyObject *op) } #define _Py_IsImmutable(op) _Py_IsImmutable(_PyObject_CAST(op)) +#ifndef Py_LIMITED_API // Cold path for Py_CHECKWRITE: called when the object is immutable. // Returns 1 if the object is still writable (ImmModule or finalizing). PyAPI_FUNC(int) _Py_CheckWriteImmutable(PyObject *op); -// Artifact[Implementation]: The definition of the `Py_CHECKWRITE` macro // Check whether an object is writeable. // The fast path just checks the immutable flag; the ImmModule and // finalizing checks are pushed into a cold out-of-line function. @@ -159,6 +159,7 @@ static inline int Py_CHECKWRITE(PyObject *op) } #define Py_CHECKWRITE(op) Py_CHECKWRITE(_PyObject_CAST(op)) #define Py_REQUIREWRITE(op, msg) {if (Py_CHECKWRITE(op)) { _PyObject_ASSERT_FAILED_MSG(op, msg); }} +#endif // !Py_LIMITED_API static inline Py_ALWAYS_INLINE void _Py_CLEAR_IMMUTABLE(PyObject *op) {