Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
675bec5
gh-92810: Avoid O(n^2) complexity in ABCMeta.__subclasscheck__
dolfinus Mar 30, 2025
701ecc9
gh-92810: Apply fixes
dolfinus Mar 31, 2025
041f109
gh-92810: Apply fixes
dolfinus Mar 31, 2025
9bc4385
gh-92810: Apply fixes
dolfinus Mar 31, 2025
3d80b1e
gh-92810: Apply fixes
dolfinus Mar 31, 2025
b7603e0
gh-92810: Return __subclasses__clause back
dolfinus Apr 21, 2025
dd0d18c
gh-92810: Revert _abc.c changes
dolfinus Apr 21, 2025
8d695fd
gh-92810: Fix linter errors
dolfinus Apr 21, 2025
a2650b6
gh-92810: Add recursive issubclass check to _abc.c
dolfinus Jun 13, 2025
7afa5ea
gh-92810: Remove WeakKeyDictionary from _py_abc
dolfinus Jun 13, 2025
57980d3
gh-92810: Add news entry
dolfinus Jun 13, 2025
bbaf38a
gh-92810: Fix news entry
dolfinus Jun 13, 2025
6fc994d
gh-92810: Fixes after review
dolfinus Jun 22, 2025
b3b5895
gh-92810: Fixes after review
dolfinus Jun 22, 2025
69c5038
gh-92810: Fixes after review
dolfinus Jun 23, 2025
dc1b6d5
gh-92810: Fixes after review
dolfinus Jun 23, 2025
cd097ab
gh-92810: Introduce FT wrappers for uint64_t atomics
dolfinus Jun 23, 2025
f3a21a7
gh-92810: Use FT atomic wrappers for ABC invalidation counter
dolfinus Jun 23, 2025
e24e815
gh-92810: Fix missing FT wrapper
dolfinus Jun 23, 2025
b723912
gh-92810: Address review fixes
dolfinus Aug 4, 2025
0295846
Merge branch 'main' into improvement/ABCMeta_subclasscheck
dolfinus Aug 4, 2025
16f39bd
gh-92810: Address review fixes
dolfinus Aug 4, 2025
2dc6453
gh-92810: Add What's New entry
dolfinus Aug 4, 2025
968766d
gh-92810: Fix What's New entry syntax
dolfinus Aug 4, 2025
a6e4461
gh-92810: Address review fixes
dolfinus Aug 4, 2025
80d3281
gh-92810: Address review fixes
dolfinus Aug 4, 2025
ff38b9e
gh-92810: Properly reset recursion check
dolfinus Aug 4, 2025
23df287
Merge branch 'main' into improvement/ABCMeta_subclasscheck
dolfinus Aug 4, 2025
0bf7374
Merge branch 'main' into improvement/ABCMeta_subclasscheck
dolfinus Aug 7, 2025
d537859
Merge branch 'main' into improvement/ABCMeta_subclasscheck
dolfinus Sep 9, 2025
d17de08
Merge branch 'main' into improvement/ABCMeta_subclasscheck
dolfinus Nov 4, 2025
858d4c0
gh-92810: Fix What's New entry
dolfinus Nov 4, 2025
7b6a0dc
gh-92810: Improve nested subclass check performance
dolfinus Nov 4, 2025
2f8f2b2
gh-92810: Automatically add ABC registry entries to cache
dolfinus Nov 4, 2025
b657cd6
gh-92810: Remove false fastpath
dolfinus Nov 6, 2025
854f2f6
Merge branch 'python:main' into improvement/ABCMeta_subclasscheck
dolfinus Nov 18, 2025
c4fe83a
gh-92810: Improve test_custom_subclasses
dolfinus Nov 30, 2025
84c5893
gh-92810: Improve test_custom_subclasses
dolfinus Nov 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,18 @@ xml.parsers.expat

.. _billion laughs: https://en.wikipedia.org/wiki/Billion_laughs_attack

abc
---

* Reduce memory usage of :func:`issubclass` checks for classes inheriting abstract classes.

:class:`abc.ABCMeta` hook ``__subclasscheck__`` now includes
a guard which is triggered then the hook is called from a parent class
(``issubclass(cls, RootClass)`` -> ``issubclass(cls, NestedClass)`` -> ...).
This guard prevents adding ``cls`` to ``NestedClass`` positive and negative caches,
preventing memory bloat in some cases (thousands of classes inherited from ABC).

(Contributed by Maxim Martynov in :gh:`92810`.)

zlib
----
Expand Down
9 changes: 9 additions & 0 deletions Include/internal/pycore_pyatomic_ft_wrappers.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ extern "C" {
_Py_atomic_load_uint16_relaxed(&value)
#define FT_ATOMIC_LOAD_UINT32_RELAXED(value) \
_Py_atomic_load_uint32_relaxed(&value)
#define FT_ATOMIC_LOAD_UINT64_RELAXED(value) \
_Py_atomic_load_uint64_relaxed(&value)
#define FT_ATOMIC_LOAD_ULONG_RELAXED(value) \
_Py_atomic_load_ulong_relaxed(&value)
#define FT_ATOMIC_STORE_PTR_RELAXED(value, new_value) \
Expand All @@ -61,6 +63,8 @@ extern "C" {
_Py_atomic_store_uint16_relaxed(&value, new_value)
#define FT_ATOMIC_STORE_UINT32_RELAXED(value, new_value) \
_Py_atomic_store_uint32_relaxed(&value, new_value)
#define FT_ATOMIC_STORE_UINT64_RELAXED(value, new_value) \
_Py_atomic_store_uint64_relaxed(&value, new_value)
#define FT_ATOMIC_STORE_CHAR_RELAXED(value, new_value) \
_Py_atomic_store_char_relaxed(&value, new_value)
#define FT_ATOMIC_LOAD_CHAR_RELAXED(value) \
Expand Down Expand Up @@ -111,6 +115,8 @@ extern "C" {
_Py_atomic_load_ullong_relaxed(&value)
#define FT_ATOMIC_ADD_SSIZE(value, new_value) \
(void)_Py_atomic_add_ssize(&value, new_value)
#define FT_ATOMIC_ADD_UINT64(value, new_value) \
(void)_Py_atomic_add_uint64(&value, new_value)
#define FT_MUTEX_LOCK(lock) PyMutex_Lock(lock)
#define FT_MUTEX_UNLOCK(lock) PyMutex_Unlock(lock)

Expand All @@ -128,6 +134,7 @@ extern "C" {
#define FT_ATOMIC_LOAD_UINT8_RELAXED(value) value
#define FT_ATOMIC_LOAD_UINT16_RELAXED(value) value
#define FT_ATOMIC_LOAD_UINT32_RELAXED(value) value
#define FT_ATOMIC_LOAD_UINT64_RELAXED(value) value
#define FT_ATOMIC_LOAD_ULONG_RELAXED(value) value
#define FT_ATOMIC_STORE_PTR_RELAXED(value, new_value) value = new_value
#define FT_ATOMIC_STORE_PTR_RELEASE(value, new_value) value = new_value
Expand All @@ -136,6 +143,7 @@ extern "C" {
#define FT_ATOMIC_STORE_UINT8_RELAXED(value, new_value) value = new_value
#define FT_ATOMIC_STORE_UINT16_RELAXED(value, new_value) value = new_value
#define FT_ATOMIC_STORE_UINT32_RELAXED(value, new_value) value = new_value
#define FT_ATOMIC_STORE_UINT64_RELAXED(value, new_value) value = new_value
#define FT_ATOMIC_LOAD_CHAR_RELAXED(value) value
#define FT_ATOMIC_STORE_CHAR_RELAXED(value, new_value) value = new_value
#define FT_ATOMIC_LOAD_UCHAR_RELAXED(value) value
Expand All @@ -161,6 +169,7 @@ extern "C" {
#define FT_ATOMIC_LOAD_ULLONG_RELAXED(value) value
#define FT_ATOMIC_STORE_ULLONG_RELAXED(value, new_value) value = new_value
#define FT_ATOMIC_ADD_SSIZE(value, new_value) (void)(value += new_value)
#define FT_ATOMIC_ADD_UINT64(value, new_value) (void)(value += new_value)
#define FT_MUTEX_LOCK(lock) do {} while (0)
#define FT_MUTEX_UNLOCK(lock) do {} while (0)

Expand Down
26 changes: 23 additions & 3 deletions Lib/_py_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ def __new__(mcls, name, bases, namespace, /, **kwargs):
cls._abc_cache = WeakSet()
cls._abc_negative_cache = WeakSet()
cls._abc_negative_cache_version = ABCMeta._abc_invalidation_counter
cls._abc_issubclasscheck_recursive = False
return cls

def register(cls, subclass):
Expand All @@ -65,7 +66,10 @@ def register(cls, subclass):
if issubclass(cls, subclass):
# This would create a cycle, which is bad for the algorithm below
raise RuntimeError("Refusing to create an inheritance cycle")
# Add registry entry
cls._abc_registry.add(subclass)
# Automatically include cache entry
cls._abc_cache.add(subclass)
ABCMeta._abc_invalidation_counter += 1 # Invalidate negative cache
return subclass

Expand Down Expand Up @@ -139,9 +143,25 @@ def __subclasscheck__(cls, subclass):
return True
# Check if it's a subclass of a subclass (recursive)
for scls in cls.__subclasses__():
if issubclass(subclass, scls):
cls._abc_cache.add(subclass)
# If inside recursive issubclass check, avoid adding classes
# to any cache because this may drastically increase memory usage.
# Unfortunately, issubclass/__subclasscheck__ don't accept third
# argument with context, so using global context within ABCMeta.
# This is done only on first method call, next will use cache anyway.
scls_is_abc = hasattr(scls, "_abc_issubclasscheck_recursive")
if scls_is_abc:
scls._abc_issubclasscheck_recursive = True
try:
# Perform recursive check
result = issubclass(subclass, scls)
finally:
if scls_is_abc:
scls._abc_issubclasscheck_recursive = False
if result:
if not cls._abc_issubclasscheck_recursive:
cls._abc_cache.add(subclass)
return True
# No dice; update negative cache
cls._abc_negative_cache.add(subclass)
if not cls._abc_issubclasscheck_recursive:
cls._abc_negative_cache.add(subclass)
return False
Loading
Loading