Skip to content

Objects with both __slots__ and __dict__ have much larger size than needed (up to 4x) on Python 3.13 #135385

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
ariebovenberg opened this issue Jun 11, 2025 · 3 comments
Labels
3.13 bugs and security fixes 3.14 bugs and security fixes 3.15 new features, bugs and security fixes interpreter-core (Objects, Python, Grammar, and Parser dirs) performance Performance or resource usage type-bug An unexpected behavior, bug, or error

Comments

@ariebovenberg
Copy link
Contributor

ariebovenberg commented Jun 11, 2025

Bug report

Bug description:

On Python 3.13, instances that have both __slots__ and __dict__ defined (e.g. through inheritance)
can have a size that is more than four times of that in previous Python versions (e.g. 3.12).

import tracemalloc
import gc


class _Point2D:
    __slots__ = ("x", "y")


class Point3D_OnlySlots(_Point2D):
    __slots__ = ("z",)

    def __init__(self, x, y, z):
        self.x, self.y, self.z = x, y, z


class Point3D_DictAndSlots(_Point2D):
    def __init__(self, x, y, z):
        self.x, self.y, self.z = x, y, z


class Point3D_OnlyDict:
    def __init__(self, x, y, z):
        self.x, self.y, self.z = x, y, z


gc.collect()  # clear freelists
tracemalloc.start()
_ = [Point3D_OnlySlots(1, 2, 3) for _ in range(1_000_000)]
print(
    f"1M Point3D (only __slots__) instances:         {tracemalloc.get_traced_memory()[0]:_} bytes"
)

gc.collect()  # clear freelists
tracemalloc.start()
_ = [Point3D_OnlyDict(1, 2, 3) for _ in range(1_000_000)]
print(
    f"1M Point3D (only __dict__) instances:          {tracemalloc.get_traced_memory()[0]:_} bytes"
)

gc.collect()  # clear freelists
tracemalloc.start()
_ = [Point3D_DictAndSlots(1, 2, 3) for _ in range(1_000_000)]
print(
    f"1M Point3D (__dict__ and __slots__) instances: {tracemalloc.get_traced_memory()[0]:_} bytes"
)

On python 3.13.3, this prints:

1M Point3D (only __slots__) instances:         64_448_792 bytes
1M Point3D (only __dict__) instances:          104_451_928 bytes
1M Point3D (__dict__ and __slots__) instances: 416_448_848 bytes

On python 3.12.11, this prints:

1M Point3D (only __slots__) instances:         64_448_792 bytes
1M Point3D (only __dict__) instances:          96_451_808 bytes
1M Point3D (__dict__ and __slots__) instances: 96_452_232 bytes

The apparent cause seems to be the object layout changes (see here)
which introduced inline values. It appears that these don't mesh well with __slots__.
Could this be because the inline values need to have a fixed offset?
What appears to cause the dramatic increase above is that having (nonempty) __slots__ will
trigger __dict__ materialization (size 30) as soon as a non-slot attribute is set. In contrast to the optimized "no slots" case, this dict doesn't shrink as more instances are created.

Of course, mixing __slots__ and __dict__ is not recommended, but it's a trap that can be easily fallen into.
For example, if forgetting to define __slots__ anywhere in a complex class hierarchy.

I'm unsure if I'm understanding exactly what's going on though. @markshannon perhaps you can shed some light on this?

Related: #115776, #115822

CPython versions tested on:

3.13

Operating systems tested on:

macOS

Linked PRs

@ariebovenberg ariebovenberg added the type-bug An unexpected behavior, bug, or error label Jun 11, 2025
@ariebovenberg ariebovenberg changed the title Objects with __slots__ and __dict__ have much larger size than needed (up to 4x) on Python 3.13 Objects with both __slots__ and __dict__ have much larger size than needed (up to 4x) on Python 3.13 Jun 11, 2025
@ZeroIntensity ZeroIntensity added interpreter-core (Objects, Python, Grammar, and Parser dirs) 3.13 bugs and security fixes 3.14 bugs and security fixes 3.15 new features, bugs and security fixes labels Jun 11, 2025
@Eclips4 Eclips4 added the performance Performance or resource usage label Jun 11, 2025
@Eclips4
Copy link
Member

Eclips4 commented Jun 11, 2025

Hello! Thanks for the report.
Results on current main:

1M Point3D (only __slots__) instances:         64_448_952 bytes
1M Point3D (only __dict__) instances:          104_452_152 bytes
1M Point3D (__dict__ and __slots__) instances: 120_452_152 bytes

@ariebovenberg
Copy link
Contributor Author

ariebovenberg commented Jun 11, 2025

Ah, silly of me not to check the current main as well. Is this something that will be backported into 3.13?

@shuimu5418
Copy link

shuimu5418 commented Jun 11, 2025

File:Objects/typeobject.c
Func:type_ready_managed_dict()
Lines:9060-9062
Cond:if (type->tp_itemsize == 0)


EDIT: Apologies, I posted my analysis before seeing Eclips4's comment above. I have since confirmed locally by compiling and testing the main branch that this issue is indeed fixed there.

It is too difficult for me. But I think i can link these issues:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3.13 bugs and security fixes 3.14 bugs and security fixes 3.15 new features, bugs and security fixes interpreter-core (Objects, Python, Grammar, and Parser dirs) performance Performance or resource usage type-bug An unexpected behavior, bug, or error
Projects
None yet
Development

No branches or pull requests

4 participants