Skip to content

gh-92810: Reduce memory usage by ABCMeta.__subclasscheck__#131914

Open
dolfinus wants to merge 46 commits into
python:mainfrom
dolfinus:improvement/ABCMeta_subclasscheck
Open

gh-92810: Reduce memory usage by ABCMeta.__subclasscheck__#131914
dolfinus wants to merge 46 commits into
python:mainfrom
dolfinus:improvement/ABCMeta_subclasscheck

Conversation

@dolfinus
Copy link
Copy Markdown

@dolfinus dolfinus commented Mar 30, 2025

For python build using --enable-optimizations:

benchmark.py
import argparse
import gc
import os
import time
from contextlib import contextmanager
from dataclasses import dataclass, field
from typing import Callable

import psutil

WIDTH = 40


def current_memory():
    gc.collect()
    return psutil.Process(os.getpid()).memory_info().rss


def format_time(time_ns: float):
    return f"{time_ns / 10**3: >7.3f} us"

def format_memory(memory_bytes: float):
    return f"{memory_bytes / 2**20: >7.3f} MiB"


@dataclass
class Benchmark:
    testcase: str
    iterations: int
    time_ns: float = 0
    memory_diff_bytes: float = 0
    skipped: bool = False

    def skip(self):
        self.skipped = True
        return self

    @contextmanager
    def run(self):
        start_time_ns = time.time_ns()
        yield self
        self.time_ns = time.time_ns() - start_time_ns

    @contextmanager
    def memory(self):
        start_memory = current_memory()
        yield self
        self.memory_diff_bytes = current_memory() - start_memory


@dataclass
class BenchmarkCollection:
    testcase: str
    benchmarks: list[Benchmark] = field(default_factory=list)

    def add(self, benchmark: Benchmark):
        if benchmark.skipped:
            return
        self.benchmarks.append(benchmark)

    def avg_time(self):
        if not self.benchmarks:
            return 0
        return sum(benchmark.time_ns / benchmark.iterations for benchmark in self.benchmarks) / len(self.benchmarks)

    def std_time(self):
        if not self.benchmarks:
            return 0
        return sum((benchmark.time_ns / benchmark.iterations - self.avg_time()) ** 2 for benchmark in self.benchmarks) ** 0.5

    def min_time(self):
        return min((benchmark.time_ns / benchmark.iterations for benchmark in self.benchmarks), default=0)

    def max_time(self):
        return max((benchmark.time_ns / benchmark.iterations for benchmark in self.benchmarks), default=0)

    def min_memory(self):
        return min((benchmark.memory_diff_bytes for benchmark in self.benchmarks), default=0)

    def max_memory(self):
        return max((benchmark.memory_diff_bytes for benchmark in self.benchmarks), default=0)

    def report(self):
        print(f"Testcase: {self.testcase}")
        if not self.benchmarks:
            print(f"... skipped")
            print("-" * WIDTH)
            return

        print("Time:")
        print(f" avg:     {format_time(self.avg_time())} +/- {format_time(self.std_time())}")
        print(f" min/max: {format_time(self.min_time())} ... {format_time(self.max_time())}")
        print("Memory:")
        print(f" min/max: {format_memory(self.min_memory())} ... {format_memory(self.max_memory())}")
        print("-" * WIDTH)




def isinstance_parent(metaclass: type, classes: int, comparisons: int) -> Benchmark:
    benchmark = Benchmark("isinstance(child, Parent)", iterations=classes * comparisons)
    with benchmark.memory():
        class Parent(metaclass=metaclass):
            pass

        def generate_object():
            class Child(Parent):
                pass
            return Child()

        generated = [generate_object() for _ in range(classes)]
        with benchmark.run():
            for child in generated:
                for _ in range(comparisons):
                    assert isinstance(child, Parent)

    return benchmark


def issubclass_parent(metaclass: type, classes: int, comparisons: int) -> Benchmark:
    benchmark = Benchmark("issubclass(Child, Parent)", iterations=classes * comparisons)
    with benchmark.memory():
        class Parent(metaclass=metaclass):
            pass

        def generate_class():
            class Child(Parent):
                pass
            return Child

        generated = [generate_class() for _ in range(classes)]
        with benchmark.run():
            for child in generated:
                for _ in range(comparisons):
                    assert issubclass(child, Parent)

    return benchmark


def isinstance_grandparent(metaclass: type, classes: int, comparisons: int) -> Benchmark:
    benchmark = Benchmark("isinstance(child, Grandparent)", iterations=classes * comparisons)
    with benchmark.memory():
        class Grandparent(metaclass=metaclass):
            pass

        class Parent(Grandparent):
            pass

        def generate_object():
            class Child(Parent):
                pass
            return Child()

        generated = [generate_object() for _ in range(classes)]
        with benchmark.run():
            for child in generated:
                for _ in range(comparisons):
                    assert isinstance(child, Grandparent)

    return benchmark

def issubclass_grandparent(metaclass: type, classes: int, comparisons: int) -> Benchmark:
    benchmark = Benchmark("issubclass(Child, Grandparent)", iterations=classes * comparisons)
    with benchmark.memory():
        class Grandparent(metaclass=metaclass):
            pass

        class Parent(Grandparent):
            pass

        def generate_class():
            class Child(Parent):
                pass
            return Child

        generated = [generate_class() for _ in range(classes)]
        with benchmark.run():
            for child in generated:
                for _ in range(comparisons):
                    assert issubclass(child, Grandparent)

    return benchmark


def isinstance_sibling(metaclass: type, classes: int, comparisons: int) -> Benchmark:
    benchmark = Benchmark("not isinstance(child, Sibling)", iterations=classes * comparisons)
    with benchmark.memory():
        class Parent(metaclass=metaclass):
            pass

        def generate_objects():
            class Child(Parent):
                pass

            class Sibling(Parent):
                pass

            return Child(), Sibling

        generated = [generate_objects() for _ in range(classes)]
        with benchmark.run():
            for child, sibling_class in generated:
                for _ in range(comparisons):
                    assert not isinstance(child, sibling_class)

    return benchmark


def issubclass_sibling(metaclass: type, classes: int, comparisons: int) -> Benchmark:
    benchmark = Benchmark("not issubclass(Child, Sibling)", iterations=classes * comparisons)
    with benchmark.memory():
        class Parent(metaclass=metaclass):
            pass

        def generate_classes():
            class Child(Parent):
                pass

            class Sibling(Parent):
                pass

            return Child, Sibling

        generated = [generate_classes() for _ in range(classes)]
        with benchmark.run():
            for child_class, sibling_class in generated:
                for _ in range(comparisons):
                    assert not issubclass(child_class, sibling_class)

    return benchmark


def isinstance_cousin(metaclass: type, classes: int, comparisons: int) -> Benchmark:
    benchmark = Benchmark("not isinstance(child, Cousin)", iterations=classes * comparisons)
    with benchmark.memory():
        class Grandparent(metaclass=metaclass):
            pass

        class Parent(Grandparent):
            pass

        class Uncle(Grandparent):
            pass

        def generate_objects():
            class Child(Parent):
                pass

            class Cousin(Uncle):
                pass

            return Child(), Cousin

        generated = [generate_objects() for _ in range(classes)]
        with benchmark.run():
            for child, cousin_class in generated:
                for _ in range(comparisons):
                    assert not isinstance(child, cousin_class)

    return benchmark


def issubclass_cousin(metaclass: type, classes: int, comparisons: int) -> Benchmark:
    benchmark = Benchmark("not issubclass(Child, Cousin)", iterations=classes * comparisons)
    with benchmark.memory():
        class Grandparent(metaclass=metaclass):
            pass

        class Parent(Grandparent):
            pass

        class Uncle(Grandparent):
            pass

        def generate_classes():
            class Child(Parent):
                pass

            class Cousin(Uncle):
                pass

            return Child, Cousin

        generated = [generate_classes() for _ in range(classes)]
        with benchmark.run():
            for child_class, cousin_class in generated:
                for _ in range(comparisons):
                    assert not issubclass(child_class, cousin_class)

    return benchmark


def isinstance_uncle(metaclass: type, classes: int, comparisons: int) -> Benchmark:
    benchmark = Benchmark("not isinstance(child, Uncle)", iterations=classes * comparisons)
    with benchmark.memory():
        class Grandparent(metaclass=metaclass):
            pass

        class Parent(Grandparent):
            pass

        class Uncle(Grandparent):
            pass

        def generate_objects():
            class Child(Parent):
                pass

            class Cousin(Uncle):
                pass

            return Child(), Cousin

        generated = [generate_objects() for _ in range(classes)]
        with benchmark.run():
            for child, _ in generated:
                for _ in range(comparisons):
                    assert not isinstance(child, Uncle)

    return benchmark


def issubclass_uncle(metaclass: type, classes: int, comparisons: int) -> Benchmark:
    benchmark = Benchmark("not issubclass(Child, Uncle)", iterations=classes * comparisons)
    with benchmark.memory():
        class Grandparent(metaclass=metaclass):
            pass

        class Parent(Grandparent):
            pass

        class Uncle(Grandparent):
            pass

        def generate_classes():
            class Child(Parent):
                pass

            class Cousin(Uncle):
                pass

            return Child, Cousin

        generated = [generate_classes() for _ in range(classes)]
        with benchmark.run():
            for child_class, _ in generated:
                for _ in range(comparisons):
                    assert not issubclass(child_class, Uncle)

    return benchmark




def isinstance_parent_via_register(metaclass: type, classes: int, comparisons: int) -> Benchmark:
    benchmark = Benchmark("isinstance(child, Parent.register)", iterations=classes * comparisons)

    if metaclass is type:
        return benchmark.skip()

    with benchmark.memory():
        class Parent(metaclass=metaclass):
            pass

        def generate_object():
            class Child:
                pass

            Parent.register(Child)
            return Child()

        generated = [generate_object() for _ in range(classes)]
        with benchmark.run():
            for child in generated:
                for _ in range(comparisons):
                    assert isinstance(child, Parent)

    return benchmark


def issubclass_parent_via_register(metaclass: type, classes: int, comparisons: int) -> Benchmark:
    benchmark = Benchmark("issubclass(Child, Parent.register)", iterations=classes * comparisons)

    if metaclass is type:
        return benchmark.skip()

    with benchmark.memory():
        class Parent(metaclass=metaclass):
            pass

        def generate_class():
            class Child:
                pass

            Parent.register(Child)
            return Child

        generated = [generate_class() for _ in range(classes)]
        with benchmark.run():
            for child_class in generated:
                for _ in range(comparisons):
                    assert issubclass(child_class, Parent)

    return benchmark


def isinstance_grandparent_via_register(metaclass: type, classes: int, comparisons: int) -> Benchmark:
    benchmark = Benchmark("isinstance(child, Grandparent.register)", iterations=classes * comparisons)

    if metaclass is type:
        return benchmark.skip()

    with benchmark.memory():
        class Grandparent(metaclass=metaclass):
            pass

        class Parent:
            pass

        Grandparent.register(Parent)

        def generate_object():
            class Child(Parent):
                pass

            return Child()

        generated = [generate_object() for _ in range(classes)]
        with benchmark.run():
            for child in generated:
                for _ in range(comparisons):
                    assert isinstance(child, Grandparent)

    return benchmark


def issubclass_grandparent_via_register(metaclass: type, classes: int, comparisons: int) -> Benchmark:
    benchmark = Benchmark("issubclass(child, Grandparent.register)", iterations=classes * comparisons)

    if metaclass is type:
        return benchmark.skip()

    with benchmark.memory():
        class Grandparent(metaclass=metaclass):
            pass

        class Parent:
            pass

        Grandparent.register(Parent)

        def generate_class():
            class Child(Parent):
                pass

            return Child

        generated = [generate_class() for _ in range(classes)]
        with benchmark.run():
            for child in generated:
                for _ in range(comparisons):
                    assert issubclass(child, Grandparent)

    return benchmark


def isinstance_sibling_via_register(metaclass: type, classes: int, comparisons: int) -> Benchmark:
    benchmark = Benchmark("not isinstance(child, Sibling.register)", iterations=classes * comparisons)

    if metaclass is type:
        return benchmark.skip()

    with benchmark.memory():
        class Parent(metaclass=metaclass):
            pass

        def generate_object():
            class Child:
                pass

            class Sibling:
                pass

            Parent.register(Child)
            Parent.register(Sibling)
            return Child(), Sibling

        generated = [generate_object() for _ in range(classes)]
        with benchmark.run():
            for child, sibling_class in generated:
                for _ in range(comparisons):
                    assert not isinstance(child, sibling_class)

    return benchmark


def issubclass_sibling_via_register(metaclass: type, classes: int, comparisons: int) -> Benchmark:
    benchmark = Benchmark("not issubclass(Child, Sibling.register)", iterations=classes * comparisons)

    if metaclass is type:
        return benchmark.skip()

    with benchmark.memory():
        class Parent(metaclass=metaclass):
            pass

        def generate_class():
            class Child:
                pass

            class Sibling:
                pass

            Parent.register(Child)
            Parent.register(Sibling)
            return Child, Sibling

        generated = [generate_class() for _ in range(classes)]
        with benchmark.run():
            for child_class, sibling_class in generated:
                for _ in range(comparisons):
                    assert not issubclass(child_class, sibling_class)

    return benchmark


def isinstance_cousin_via_register(metaclass: type, classes: int, comparisons: int) -> Benchmark:
    benchmark = Benchmark("not isinstance(child, Cousin.register)", iterations=classes * comparisons)

    if metaclass is type:
        return benchmark.skip()

    with benchmark.memory():
        class Grandparent(metaclass=metaclass):
            pass

        class Parent(Grandparent):
            pass

        class Uncle(Grandparent):
            pass

        def generate_object():
            class Child:
                pass

            class Cousin:
                pass

            Parent.register(Child)
            Uncle.register(Cousin)
            return Child(), Cousin

        generated = [generate_object() for _ in range(classes)]
        with benchmark.run():
            for child, cousin_class in generated:
                for _ in range(comparisons):
                    assert not isinstance(child, cousin_class)

    return benchmark


def issubclass_cousin_via_register(metaclass: type, classes: int, comparisons: int) -> Benchmark:
    benchmark = Benchmark("not issubclass(child, Cousin.register)", iterations=classes * comparisons)

    if metaclass is type:
        return benchmark.skip()

    with benchmark.memory():
        class Grandparent(metaclass=metaclass):
            pass

        class Parent(Grandparent):
            pass

        class Uncle(Grandparent):
            pass

        def generate_class():
            class Child:
                pass

            class Cousin:
                pass

            Parent.register(Child)
            Uncle.register(Cousin)
            return Child, Cousin

        generated = [generate_class() for _ in range(classes)]
        with benchmark.run():
            for child, cousin_class in generated:
                for _ in range(comparisons):
                    assert not issubclass(child, cousin_class)

    return benchmark


def isinstance_uncle_via_register(metaclass: type, classes: int, comparisons: int) -> Benchmark:
    benchmark = Benchmark("not isinstance(child, Uncle.register)", iterations=classes * comparisons)

    if metaclass is type:
        return benchmark.skip()

    with benchmark.memory():
        class Grandparent(metaclass=metaclass):
            pass

        class Parent(Grandparent):
            pass

        class Uncle(Grandparent):
            pass

        def generate_object():
            class Child:
                pass

            class Cousin:
                pass

            Parent.register(Child)
            Uncle.register(Cousin)
            return Child(), Cousin

        generated = [generate_object() for _ in range(classes)]
        with benchmark.run():
            for child, _ in generated:
                for _ in range(comparisons):
                    assert not isinstance(child, Uncle)

    return benchmark


def issubclass_uncle_via_register(metaclass: type, classes: int, comparisons: int) -> Benchmark:
    benchmark = Benchmark("not issubclass(child, Uncle.register)", iterations=classes * comparisons)

    if metaclass is type:
        return benchmark.skip()

    with benchmark.memory():
        class Grandparent(metaclass=metaclass):
            pass

        class Parent(Grandparent):
            pass

        class Uncle(Grandparent):
            pass

        def generate_class():
            class Child:
                pass

            class Cousin:
                pass

            Parent.register(Child)
            Uncle.register(Cousin)
            return Child, Cousin

        generated = [generate_class() for _ in range(classes)]
        with benchmark.run():
            for child, _ in generated:
                for _ in range(comparisons):
                    assert not issubclass(child, Uncle)

    return benchmark









def isinstance_parent_via_subclasses(metaclass: type, classes: int, comparisons: int) -> Benchmark:
    benchmark = Benchmark("isinstance(child, Parent.__subclasses__)", iterations=classes * comparisons)

    if metaclass is type:
        return benchmark.skip()

    with benchmark.memory():
        class Parent(metaclass=metaclass):
            subclasses: list[type] = []

            @classmethod
            def __subclasses__(cls):
                return cls.subclasses

        def generate_object():
            class Child:
                pass

            Parent.subclasses.append(Child)
            return Child()

        generated = [generate_object() for _ in range(classes)]
        with benchmark.run():
            for child in generated:
                for _ in range(comparisons):
                    assert isinstance(child, Parent)

    return benchmark


def issubclass_parent_via_subclasses(metaclass: type, classes: int, comparisons: int) -> Benchmark:
    benchmark = Benchmark("issubclass(Child, Parent.__subclasses__)", iterations=classes * comparisons)

    if metaclass is type:
        return benchmark.skip()

    with benchmark.memory():
        class Parent(metaclass=metaclass):
            subclasses: list[type] = []

            @classmethod
            def __subclasses__(cls):
                return cls.subclasses

        def generate_class():
            class Child:
                pass

            Parent.subclasses.append(Child)
            return Child

        generated = [generate_class() for _ in range(classes)]
        with benchmark.run():
            for child_class in generated:
                for _ in range(comparisons):
                    assert issubclass(child_class, Parent)

    return benchmark


def isinstance_grandparent_via_subclasses(metaclass: type, classes: int, comparisons: int) -> Benchmark:
    benchmark = Benchmark("isinstance(child, Grandparent.__subclasses__)", iterations=classes * comparisons)

    if metaclass is type:
        return benchmark.skip()

    with benchmark.memory():
        class Grandparent(metaclass=metaclass):
            pass

        class Parent(Grandparent):
            subclasses: list[type] = []

            @classmethod
            def __subclasses__(cls):
                return cls.subclasses

        def generate_object():
            class Child:
                pass

            Parent.subclasses.append(Child)
            return Child()

        generated = [generate_object() for _ in range(classes)]
        with benchmark.run():
            for child in generated:
                for _ in range(comparisons):
                    assert isinstance(child, Grandparent)

    return benchmark


def issubclass_grandparent_via_subclasses(metaclass: type, classes: int, comparisons: int) -> Benchmark:
    benchmark = Benchmark("issubclass(child, Grandparent.__subclasses__)", iterations=classes * comparisons)

    if metaclass is type:
        return benchmark.skip()

    with benchmark.memory():
        class Grandparent(metaclass=metaclass):
            pass

        class Parent(Grandparent):
            subclasses: list[type] = []

            @classmethod
            def __subclasses__(cls):
                return cls.subclasses

        def generate_class():
            class Child:
                pass

            Parent.subclasses.append(Child)
            return Child

        generated = [generate_class() for _ in range(classes)]
        with benchmark.run():
            for child in generated:
                for _ in range(comparisons):
                    assert issubclass(child, Grandparent)

    return benchmark


def isinstance_sibling_via_subclasses(metaclass: type, classes: int, comparisons: int) -> Benchmark:
    benchmark = Benchmark("not isinstance(child, Sibling.__subclasses__)", iterations=classes * comparisons)

    if metaclass is type:
        return benchmark.skip()

    with benchmark.memory():
        class Parent(metaclass=metaclass):
            subclasses: list[type] = []

            @classmethod
            def __subclasses__(cls):
                return cls.subclasses

        def generate_object():
            class Child:
                pass

            class Sibling:
                pass

            Parent.subclasses.append(Child)
            Parent.subclasses.append(Sibling)
            return Child(), Sibling

        generated = [generate_object() for _ in range(classes)]
        with benchmark.run():
            for child, sibling_class in generated:
                for _ in range(comparisons):
                    assert not isinstance(child, sibling_class)

    return benchmark


def issubclass_sibling_via_subclasses(metaclass: type, classes: int, comparisons: int) -> Benchmark:
    benchmark = Benchmark("not issubclass(Child, Sigling.__subclasses__)", iterations=classes * comparisons)

    if metaclass is type:
        return benchmark.skip()

    with benchmark.memory():
        class Parent(metaclass=metaclass):
            subclasses: list[type] = []

            @classmethod
            def __subclasses__(cls):
                return cls.subclasses

        def generate_class():
            class Child:
                pass

            class Sibling:
                pass

            Parent.subclasses.append(Child)
            Parent.subclasses.append(Sibling)
            return Child, Sibling

        generated = [generate_class() for _ in range(classes)]
        with benchmark.run():
            for child_class, sibling_class in generated:
                for _ in range(comparisons):
                    assert not issubclass(child_class, sibling_class)

    return benchmark


def isinstance_cousin_via_subclasses(metaclass: type, classes: int, comparisons: int) -> Benchmark:
    benchmark = Benchmark("not isinstance(child, Cousin.__subclasses__)", iterations=classes * comparisons)

    if metaclass is type:
        return benchmark.skip()

    with benchmark.memory():
        class Grandparent(metaclass=metaclass):
            pass

        class Parent(Grandparent):
            subclasses: list[type] = []

            @classmethod
            def __subclasses__(cls):
                return cls.subclasses

        class Uncle(Grandparent):
            subclasses: list[type] = []

            @classmethod
            def __subclasses__(cls):
                return cls.subclasses

        def generate_object():
            class Child:
                pass

            class Cousin:
                pass

            Parent.subclasses.append(Child)
            Uncle.subclasses.append(Cousin)
            return Child(), Cousin

        generated = [generate_object() for _ in range(classes)]
        with benchmark.run():
            for child, cousin_class in generated:
                for _ in range(comparisons):
                    assert not isinstance(child, cousin_class)

    return benchmark


def issubclass_cousin_via_subclasses(metaclass: type, classes: int, comparisons: int) -> Benchmark:
    benchmark = Benchmark("not issubclass(child, Cousin.__subclasses__)", iterations=classes * comparisons)

    if metaclass is type:
        return benchmark.skip()

    with benchmark.memory():
        class Grandparent(metaclass=metaclass):
            pass

        class Parent(Grandparent):
            subclasses: list[type] = []

            @classmethod
            def __subclasses__(cls):
                return cls.subclasses

        class Uncle(Grandparent):
            subclasses: list[type] = []

            @classmethod
            def __subclasses__(cls):
                return cls.subclasses

        def generate_class():
            class Child:
                pass

            class Cousin:
                pass

            Parent.subclasses.append(Child)
            Uncle.subclasses.append(Cousin)
            return Child, Cousin

        generated = [generate_class() for _ in range(classes)]
        with benchmark.run():
            for child, cousin_class in generated:
                for _ in range(comparisons):
                    assert not issubclass(child, cousin_class)

    return benchmark


def isinstance_uncle_via_subclasses(metaclass: type, classes: int, comparisons: int) -> Benchmark:
    benchmark = Benchmark("not isinstance(child, Uncle.__subclasses__)", iterations=classes * comparisons)

    if metaclass is type:
        return benchmark.skip()

    with benchmark.memory():
        class Grandparent(metaclass=metaclass):
            pass

        class Parent(Grandparent):
            subclasses: list[type] = []

            @classmethod
            def __subclasses__(cls):
                return cls.subclasses

        class Uncle(Grandparent):
            subclasses: list[type] = []

            @classmethod
            def __subclasses__(cls):
                return cls.subclasses

        def generate_object():
            class Child:
                pass

            class Cousin:
                pass

            Parent.subclasses.append(Child)
            Uncle.subclasses.append(Cousin)
            return Child(), Cousin

        generated = [generate_object() for _ in range(classes)]
        with benchmark.run():
            for child, _ in generated:
                for _ in range(comparisons):
                    assert not isinstance(child, Uncle)

    return benchmark


def issubclass_uncle_via_subclasses(metaclass: type, classes: int, comparisons: int) -> Benchmark:
    benchmark = Benchmark("not issubclass(child, Uncle.__subclasses__)", iterations=classes * comparisons)

    if metaclass is type:
        return benchmark.skip()

    with benchmark.memory():
        class Grandparent(metaclass=metaclass):
            pass

        class Parent(Grandparent):
            subclasses: list[type] = []

            @classmethod
            def __subclasses__(cls):
                return cls.subclasses

        class Uncle(Grandparent):
            subclasses: list[type] = []

            @classmethod
            def __subclasses__(cls):
                return cls.subclasses

        def generate_class():
            class Child:
                pass

            class Cousin:
                pass

            Parent.subclasses.append(Child)
            Uncle.subclasses.append(Cousin)
            return Child, Cousin

        generated = [generate_class() for _ in range(classes)]
        with benchmark.run():
            for child, _ in generated:
                for _ in range(comparisons):
                    assert not issubclass(child, Uncle)

    return benchmark




def isinstance_unrelated(metaclass: type, classes: int, comparisons: int) -> Benchmark:
    benchmark = Benchmark("isinstance(child, Unrelated)", iterations=classes * comparisons)
    with benchmark.memory():
        class Parent(metaclass=metaclass):
            pass

        class Unrelated: pass

        def generate_object():
            class Child(Parent):
                pass
            return Child()

        generated = [generate_object() for _ in range(classes)]
        with benchmark.run():
            for child in generated:
                for _ in range(comparisons):
                    assert not isinstance(child, Unrelated)

    return benchmark


def issubclass_unrelated(metaclass: type, classes: int, comparisons: int) -> Benchmark:
    benchmark = Benchmark("issubclass(Child, Unrelated)", iterations=classes * comparisons)
    with benchmark.memory():
        class Parent(metaclass=metaclass):
            pass

        class Unrelated: pass

        def generate_class():
            class Child(Parent):
                pass
            return Child

        generated = [generate_class() for _ in range(classes)]
        with benchmark.run():
            for child in generated:
                for _ in range(comparisons):
                    assert not issubclass(child, Unrelated)

    return benchmark




def isinstance_unrelated_abc(metaclass: type, classes: int, comparisons: int) -> Benchmark:
    benchmark = Benchmark("isinstance(child, AnotherABC)", iterations=classes * comparisons)
    with benchmark.memory():
        class Parent(metaclass=metaclass):
            pass

        class AnotherABC(metaclass=metaclass):
            pass

        def generate_object():
            class Child(Parent):
                pass
            return Child()

        generated = [generate_object() for _ in range(classes)]
        with benchmark.run():
            for child in generated:
                for _ in range(comparisons):
                    assert not isinstance(child, AnotherABC)

    return benchmark


def issubclass_unrelated_abc(metaclass: type, classes: int, comparisons: int) -> Benchmark:
    benchmark = Benchmark("issubclass(Child, AnotherABC)", iterations=classes * comparisons)
    with benchmark.memory():
        class Parent(metaclass=metaclass):
            pass

        class AnotherABC(metaclass=metaclass):
            pass

        def generate_class():
            class Child(Parent):
                pass
            return Child

        generated = [generate_class() for _ in range(classes)]
        with benchmark.run():
            for child in generated:
                for _ in range(comparisons):
                    assert not issubclass(child, AnotherABC)

    return benchmark





test_cases = [
    isinstance_parent,
    issubclass_parent,
    isinstance_grandparent,
    issubclass_grandparent,
    isinstance_sibling,
    issubclass_sibling,
    isinstance_cousin,
    issubclass_cousin,
    isinstance_uncle,
    issubclass_uncle,
    #
    isinstance_parent_via_register,
    issubclass_parent_via_register,
    isinstance_grandparent_via_register,
    issubclass_grandparent_via_register,
    isinstance_sibling_via_register,
    issubclass_sibling_via_register,
    isinstance_cousin_via_register,
    issubclass_cousin_via_register,
    isinstance_uncle_via_register,
    issubclass_uncle_via_register,
    #
    isinstance_parent_via_subclasses,
    issubclass_parent_via_subclasses,
    isinstance_grandparent_via_subclasses,
    issubclass_grandparent_via_subclasses,
    isinstance_sibling_via_subclasses,
    issubclass_sibling_via_subclasses,
    isinstance_cousin_via_subclasses,
    issubclass_cousin_via_subclasses,
    isinstance_uncle_via_subclasses,
    issubclass_uncle_via_subclasses,
    #
    isinstance_unrelated,
    issubclass_unrelated,
    isinstance_unrelated_abc,
    issubclass_unrelated_abc,
]

def get_parser():
    parser = argparse.ArgumentParser()
    parser.add_argument("--metaclass", type=str, default="abc.ABCMeta", choices=["abc.ABCMeta", "_py_abc.ABCMeta", "builtins.type"], help="ABCMeta implementation")
    parser.add_argument("--case", nargs="*", type=str, default="all", help="Test case name, or 'isinstance' or 'issubclass', or 'all'")
    parser.add_argument("--rounds", type=int, default=3, help="Number of times to run each test case")
    parser.add_argument("--classes", type=int, default=3_000, help="Number of classes to generate within each test case")
    parser.add_argument("--comparisons", type=int, default=1000, help="Number of per-class comparisons within each test case")
    return parser


def parse_args(argv=None):
    cases: dict[str, list[Callable]] = {case.__name__: [case] for case in test_cases}
    cases["isinstance"] = [case for case in test_cases if "isinstance" in case.__name__]
    cases["issubclass"] = [case for case in test_cases if "issubclass" in case.__name__]
    cases["all"] = test_cases

    parser = get_parser()
    args = parser.parse_args(argv)
    selected = []
    for name, functions in cases.items():
        for func in functions:
            if name in args.case:
                selected.append(func)

    module, klass = args.metaclass.rsplit(".", 1)
    metaclass = getattr(__import__(module), klass)

    return metaclass, selected, args.rounds, args.classes, args.comparisons


if __name__ == "__main__":
    metaclass, selected, rounds, classes, comparisons = parse_args()
    print(f"Implementation: {metaclass}")
    print(f"Rounds: {rounds}")
    print(f"Classes: {classes}")
    print(f"Comparisons: {comparisons}")

    print("=" * WIDTH)
    print(f"Memory before tests: {format_memory(current_memory())}")
    results: dict[str, BenchmarkCollection] = {}
    for testcase in selected:
        for _ in range(rounds):
            benchmark = testcase(metaclass, classes, comparisons)
            if testcase.__name__ not in results:
                results[testcase.__name__] = BenchmarkCollection(benchmark.testcase)
            results[testcase.__name__].add(benchmark)
    print(f"Memory after tests: {format_memory(current_memory())}")
    print("=" * WIDTH)

    for item in results.values():
        item.report()
sudo ./python -m pyperf system tune
taskset -c 0 ./python benchmark.py --metaclass abc.ABCMeta --rounds 3 --classes 5000
taskset -c 0 ./python benchmark.py --metaclass _py_abc.ABCMeta --rounds 3 --classes 5000
Impl Max memory before, MB Max memory after, MB
_abc 6331 50
_py_abc 4422 60
Impl Total time before Total time after
_abc 6m 16s 2m 19s
_py_abc 8m 42s 6m 45s
Check Impl before after Impl before after
isinstance(child, Parent) _abc 0.115us
4MiB...15MiB
0.121us
4MiB...15MiB
_py_abc 0.212us
11MiB...24MiB
0.219us
10MiB...24MiB
issubclass(Child, Parent) _abc 0.105us
0MiB...1MiB
0.115us
0MiB...1MiB
_py_abc 0.203us
6MiB...8MiB
0.214us
5MiB...8MiB
isinstance(child, Grandparent) _abc 0.112us
0MiB...2MiB
0.121us
0MiB...1MiB
_py_abc 0.210us
4MiB...7MiB
0.216us
4MiB...7MiB
issubclass(Child, Grandparent) _abc 0.103us
0MiB
0.110us
0MiB
_py_abc 0.201us
0MiB...1MiB
0.212us
0MiB...1MiB
not isinstance(child, Sibling) _abc 0.113us
4MiB...14MiB
0.123us
2MiB...14MiB
_py_abc 0.348us
13MiB...23MiB
0.362us
13MiB...22MiB
not issubclass(Child, Sibling) _abc 0.105us
1MiB
0.110us
0MiB...1MiB
_py_abc 0.328us
8MiB...10MiB
0.342us
8MiB...11MiB
not isinstance(child, Cousin) _abc 0.115us
1MiB...2MiB
0.121us
1MiB
_py_abc 0.350us
7MiB...9MiB
0.361us
7MiB...9MiB
not issubclass(Child, Cousin) _abc 0.104us
0MiB
0.110us
0MiB...1MiB
_py_abc 0.329us
4MiB
0.341us
3MiB...4MiB
not isinstance(child, Uncle) _abc 7.268us
6174MiB...6333MiB
2.077us
0MiB...1MiB
_py_abc 9.957us
4382MiB...4422MiB
6.677us
6MiB
not issubclass(Child, Uncle) _abc 7.099us
6171MiB
2.054us
0MiB
_py_abc 9.936us
4380MiB
6.701us
4MiB

Memory increment is measured during isinstance() / issubclass() calls, not during preparation, like class creation or registration where actual registry allocation is performed. So memory usage in tables below is almost always 0.

Timing drop for 2 first rows is mostly due to cls._abc_cache.add(scls) call within def register(slc, scls) which wasn't implemented before.

Check Impl before after Impl before after
isinstance(child, Parent.register) _abc 0.273us
0MiB
0.122us
0MiB
_py_abc 0.440us
0MiB
0.270us
0MiB
issubclass(Child, Parent.register) _abc 0.154us
0MiB
0.115us
0MiB
_py_abc 0.427us
0MiB
0.259us
0MiB
isinstance(child, Grandparent.register) _abc 0.114us
0MiB
0.120us
0MiB
_py_abc 0.253us
0MiB
0.259us
0MiB
issubclass(Child, Grandparent.register) _abc 0.103us
0MiB
0.110us
0MiB
_py_abc 0.240us
0MiB
0.247us
0MiB
not isinstance(child, Sibling.register) _abc 0.027us
0MiB
0.028us
1MiB
_py_abc 0.028us
0MiB
0.028us
2MiB
not issubclass(Child, Sibling.register) _abc 0.018us
0MiB
0.018us
1MiB
_py_abc 0.018us
0MiB
0.018us
2MiB
not isinstance(child, Cousin.register) _abc 0.028us
0MiB
0.028us
2MiB
_py_abc 0.028us
0MiB
0.028us
3MiB
not issubclass(Child, Cousin.register) _abc 0.018us
0MiB
0.018us
2MiB
_py_abc 0.019us
0MiB
0.020us
3MiB
not isinstance(child, Uncle.register) _abc 0.249us
0MiB
0.245us
2MiB...3MiB
_py_abc 0.843us
0MiB
0.849us
4MiB
not issubclass(Child, Uncle.register) _abc 0.238us
0MiB
0.240us
2MiB
_py_abc 0.815us
0MiB
0.815us
4MiB

This became a bit slower due to new checks, but this is rare case.

Check Impl before after Impl before after
isinstance(child, Parent.__subclasses__) _abc 0.128us
0MiB
0.160us
0MiB
_py_abc 0.305us
0MiB
0.384us
0MiB
issubclass(Child, Parent.__subclasses__) _abc 0.118us
0MiB
0.150us
0MiB
_py_abc 0.293us
0MiB
0.373us
0MiB
isinstance(child, Grandparent.__subclasses__) _abc 0.126us
0MiB
0.160us
0MiB
_py_abc 0.304us
0MiB
0.387us
0MiB
issubclass(Child, Grandparent.__subclasses__) _abc 0.116us
0MiB
0.147us
0MiB
_py_abc 0.293us
0MiB
0.376us
0MiB
not isinstance(child, Sibling.__subclasses__) _abc 0.028us
0MiB
0.028us
0MiB
_py_abc 0.028us
0MiB
0.028us
1MiB
not issubclass(Child, Sibling.__subclasses__) _abc 0.018us
0MiB
0.018us
0MiB
_py_abc 0.018us
0MiB
0.018us
1MiB
not isinstance(child, Cousin.__subclasses__) _abc 0.028us
0MiB
0.028us
0MiB
_py_abc 0.028us
0MiB
0.028us
1MiB
not issubclass(Child, Cousin.__subclasses__) _abc 0.018us
0MiB
0.018us
0MiB
_py_abc 0.018us
0MiB
0.018us
1MiB
not isinstance(child, Uncle.__subclasses__) _abc 0.145us
0MiB
0.251us
1MiB
_py_abc 0.553us
0MiB
0.766us
2MiB
not issubclass(Child, Uncle.__subclasses__) _abc 0.134us
0MiB
0.241us
0MiB
_py_abc 0.526us
0MiB
0.739us
2MiB

Just to check that nothing is broken:

Check Impl before after Impl before after
not isinstance(child, Unrelated) _abc 0.028us
0MiB
0.028us
0MiB
_py_abc 0.028us
0MiB
0.028us
0MiB
not issubclass(Child, Unrelated) _abc 0.018us
0MiB
0.018us
0MiB
_py_abc 0.018us
0MiB
0.018us
0MiB
not isinstance(child, UnrelatedABC) _abc 0.118us
0MiB
0.119us
0MiB
_py_abc 0.477us
0MiB
0.472us
0MiB
not issubclass(Child, UnrelatedABC) _abc 0.110us
0MiB
0.109us
0MiB
_py_abc 0.450us
0MiB
0.450us
0MiB

Flamegraphs for _py_abc impl and test issubclass_uncle (the most time and memory consuming case on main):
main_vs_pr131914.tar.gz

Alternatives:

@bedevere-app
Copy link
Copy Markdown

bedevere-app Bot commented Mar 30, 2025

Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool.

If this change has little impact on Python users, wait for a maintainer to apply the skip news label instead.

Comment thread Lib/_py_abc.py Outdated
Comment thread Lib/_py_abc.py Outdated
Comment thread Modules/_abc.c Outdated
Comment thread Modules/_abc.c Outdated
Comment thread Modules/_abc.c Outdated
Comment thread Modules/_abc.c Outdated
Comment thread Modules/_abc.c Outdated
Comment thread Modules/_abc.c Outdated
Comment thread Lib/test/test_abstract_numbers.py
Comment thread Lib/test/test_abc.py Outdated
@bedevere-app
Copy link
Copy Markdown

bedevere-app Bot commented Mar 31, 2025

Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool.

If this change has little impact on Python users, wait for a maintainer to apply the skip news label instead.

3 similar comments
@bedevere-app
Copy link
Copy Markdown

bedevere-app Bot commented Mar 31, 2025

Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool.

If this change has little impact on Python users, wait for a maintainer to apply the skip news label instead.

@bedevere-app
Copy link
Copy Markdown

bedevere-app Bot commented Mar 31, 2025

Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool.

If this change has little impact on Python users, wait for a maintainer to apply the skip news label instead.

@bedevere-app
Copy link
Copy Markdown

bedevere-app Bot commented Mar 31, 2025

Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool.

If this change has little impact on Python users, wait for a maintainer to apply the skip news label instead.

Comment thread Lib/test/test_abc.py
Comment thread Lib/test/test_abc.py Outdated
Comment thread Lib/_py_abc.py Outdated
Comment thread Lib/test/test_isinstance.py Outdated
Comment thread Lib/test/test_isinstance.py
@python-cla-bot
Copy link
Copy Markdown

python-cla-bot Bot commented Apr 6, 2025

All commit authors signed the Contributor License Agreement.

CLA signed

@bedevere-app
Copy link
Copy Markdown

bedevere-app Bot commented Apr 21, 2025

Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool.

If this change has little impact on Python users, wait for a maintainer to apply the skip news label instead.

Signed-off-by: Martynov Maxim <martinov_m_s_@mail.ru>
Signed-off-by: Martynov Maxim <martinov_m_s_@mail.ru>
Signed-off-by: Martynov Maxim <martinov_m_s_@mail.ru>
Signed-off-by: Martynov Maxim <martinov_m_s_@mail.ru>
Signed-off-by: Martynov Maxim <martinov_m_s_@mail.ru>
@dolfinus dolfinus force-pushed the improvement/ABCMeta_subclasscheck branch from abf4bfe to b7603e0 Compare April 21, 2025 11:03
@bedevere-app
Copy link
Copy Markdown

bedevere-app Bot commented Apr 21, 2025

Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool.

If this change has little impact on Python users, wait for a maintainer to apply the skip news label instead.

@bedevere-app
Copy link
Copy Markdown

bedevere-app Bot commented Apr 21, 2025

Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool.

If this change has little impact on Python users, wait for a maintainer to apply the skip news label instead.

@bedevere-app
Copy link
Copy Markdown

bedevere-app Bot commented Apr 21, 2025

Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool.

If this change has little impact on Python users, wait for a maintainer to apply the skip news label instead.

@dolfinus dolfinus changed the title gh-92810: Avoid O(n^2) complexity in ABCMeta.__subclasscheck__ gh-92810: Reduce memory usage by ABCMeta.__subclasscheck__ Apr 23, 2025
@bedevere-app
Copy link
Copy Markdown

bedevere-app Bot commented Jun 13, 2025

Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool.

If this change has little impact on Python users, wait for a maintainer to apply the skip news label instead.

@bedevere-app
Copy link
Copy Markdown

bedevere-app Bot commented Jun 13, 2025

Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool.

If this change has little impact on Python users, wait for a maintainer to apply the skip news label instead.

@dolfinus dolfinus requested a review from picnixz November 24, 2025 22:05
@dolfinus
Copy link
Copy Markdown
Author

dolfinus commented Dec 4, 2025

Alternative implementation which eliminates for scls in cls.__subclasses__() in by bubbling up cls.register(subclass) up to the root ABC class: #141171. IMHO that's a more elegant solution.

UPD: implementation from #150540 removes for scls in cls.__subclasses__() clause completely.

@picnixz
Copy link
Copy Markdown
Member

picnixz commented Dec 14, 2025

I've seen your work but I really don't have enough time to estimate the impact for all cases. It's not that it's a bad change but ABC is extremely used and even a tiny change should be carefully considered. Now that I also have a job, I've got less time for reviews so I'd like to ask @JelleZijlstra to possibly have a look at either this PR or the alternative if he's available.

  • If we consider this a bugfix, we can take as much time as we want (but I think it's better to consider this a new feature even if it's fixing performance issues)
  • If we consider this a new feature for 3.15, we have until May 2026.

@dolfinus
Copy link
Copy Markdown
Author

dolfinus commented Dec 14, 2025

Thanks @picnixz, I understand your point. There is no rush here, the change can be landed any time.
Regarding potential ABCMeta behavior change, I can split this PR into 2 smaller ones - one including only new tests, and another one with recursion guard, to show that behavior remains the same.

Comment thread Doc/whatsnew/3.15.rst Outdated
Comment thread Lib/test/test_abc.py Outdated
@kumaraditya303
Copy link
Copy Markdown
Contributor

Regarding potential ABCMeta behavior change, I can split this PR into 2 smaller ones - one including only new tests, and another one with recursion guard, to show that behavior remains the same.

+1 That sounds good. The PR with new tests can be merged as is and then we can continue working on this ensure that the behavior remains same.

@dolfinus
Copy link
Copy Markdown
Author

Extracted tests into #144941

@picnixz picnixz removed their request for review March 1, 2026 15:56
@dolfinus
Copy link
Copy Markdown
Author

dolfinus commented Apr 8, 2026

@picnixz gentle ping about this and related PRs

@picnixz
Copy link
Copy Markdown
Member

picnixz commented Apr 8, 2026

I'm sorry but as I said, I don't feel confident enough to accept the change now and I would prefer that other core devs express their opinions. Now, AFAIR, and AFAICT, most common cases are faster and consume less memory right? Does this change actually affect performance when we try to do instance/subclass checks for totally unrelated classes? (in the presented benchmarks, there is a common ancestor, but what if we have two entirely different trees, is there anything slowing down those paths?)

@dolfinus
Copy link
Copy Markdown
Author

dolfinus commented Apr 8, 2026

Ok, I'll add this case in benchmark a bit later.

What do you think about #144941? It does not contain any changes in abc module source code, only new tests.

@github-actions
Copy link
Copy Markdown

This PR is stale because it has been open for 30 days with no activity.

@github-actions github-actions Bot added the stale Stale PR or inactive for long period of time. label May 14, 2026
@read-the-docs-community
Copy link
Copy Markdown

read-the-docs-community Bot commented May 27, 2026

Documentation build overview

📚 cpython-previews | 🛠️ Build #32877818 | 📁 Comparing 1a3e0c4 against main (24c6bbc)

  🔍 Preview build  

2 files changed
± whatsnew/3.15.html
± whatsnew/changelog.html

@dolfinus
Copy link
Copy Markdown
Author

dolfinus commented May 27, 2026

Does this change actually affect performance when we try to do instance/subclass checks for totally unrelated classes? (in the presented benchmarks, there is a common ancestor, but what if we have two entirely different trees, is there anything slowing down those paths?)

Added this test case to benchmark, timing and RAM usage remains the same. Also resolved conflicts with main branch.

@dolfinus
Copy link
Copy Markdown
Author

dolfinus commented May 28, 2026

Note that there are 2 other implementations:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting review stale Stale PR or inactive for long period of time.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants