Skip to content

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

Description

@ariebovenberg

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

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    3.13only security fixes3.14bugs and security fixesinterpreter-core(Objects, Python, Grammar, and Parser dirs)performancePerformance or resource usagetype-bugAn unexpected behavior, bug, or error

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions