Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,19 @@ arguments (:pep:`791`).
Improved modules
================

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`.)

argparse
--------

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
38 changes: 33 additions & 5 deletions Lib/_py_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ 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

# Performance optimization for common case
cls._abc_should_check_subclasses = False
if "__subclasses__" in namespace:
cls._abc_should_check_subclasses = True
for base in bases:
if hasattr(base, "_abc_should_check_subclasses"):
base._abc_should_check_subclasses = True
return cls

def register(cls, subclass):
Expand All @@ -65,8 +73,20 @@ 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")
# Actual registration
cls._abc_registry.add(subclass)
ABCMeta._abc_invalidation_counter += 1 # Invalidate negative cache
# Recursively register the subclass in all ABC bases,
# to avoid recursive lookups down the class tree.
# >>> class Ancestor1(ABC): pass
# >>> class Ancestor2(Ancestor1): pass
# >>> class Other: pass
# >>> Ancestor2.register(Other) # calls Ancestor1.register(Other)
# >>> issubclass(Other, Ancestor2) is True
# >>> issubclass(Other, Ancestor1) is True # already in registry
for base in cls.__bases__:
if hasattr(base, "_abc_registry"):
base.register(subclass)
return subclass

def _dump_registry(cls, file=None):
Expand Down Expand Up @@ -133,15 +153,23 @@ def __subclasscheck__(cls, subclass):
cls._abc_cache.add(subclass)
return True
# Check if it's a subclass of a registered class (recursive)
if subclass in cls._abc_registry:
cls._abc_cache.add(subclass)
return True
for rcls in cls._abc_registry:
if issubclass(subclass, rcls):
cls._abc_cache.add(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)
return True
# Check if it's a subclass of a subclass (recursive).
# If __subclasses__ contain only ABCs,
# calling issubclass(...) will trigger the same __subclasscheck__
# on *every* element of class inheritance tree.
# Performing that only in resence of `def __subclasses__()` classmethod
if cls._abc_should_check_subclasses:
for scls in cls.__subclasses__():
if issubclass(subclass, scls):
cls._abc_cache.add(subclass)
return True
# No dice; update negative cache
cls._abc_negative_cache.add(subclass)
return False
2 changes: 1 addition & 1 deletion Lib/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ class ABCMeta(type):
"""
def __new__(mcls, name, bases, namespace, /, **kwargs):
cls = super().__new__(mcls, name, bases, namespace, **kwargs)
_abc_init(cls)
_abc_init(cls, bases, namespace)
return cls

def register(cls, subclass):
Expand Down
Loading
Loading