Какие ресурсы использует экземпляр класса? - PullRequest
14 голосов
/ 13 июня 2019

Насколько эффективен python (я полагаю, cpython) при распределении ресурсов для вновь созданного экземпляра класса? У меня есть ситуация, когда мне нужно будет создать экземпляр класса узла миллионы раз, чтобы создать древовидную структуру. Каждый из объектов узла должен быть легковесным, просто содержать несколько чисел и ссылки на родительские и дочерние узлы.

Например, будет ли python выделять память для всех свойств "двойного подчеркивания" каждого экземпляра объекта (например, строк документации, __dict__, __repr__, __class__ и т. Д. И т. Д.), Либо для создания этих свойства по отдельности или хранить указатели на то, где они определены классом? Или это эффективно и не требует хранения ничего, кроме определенных мной вещей, которые нужно хранить в каждом объекте?

Ответы [ 4 ]

12 голосов
/ 17 июня 2019

Внешне это довольно просто: методы, переменные класса и строка документации класса хранятся в классе (строки функции хранятся в функции). Переменные экземпляра хранятся в экземпляре. Экземпляр также ссылается на класс, чтобы вы могли искать методы. Как правило, все они хранятся в словарях (__dict__).

Так что да, краткий ответ: Python не хранит методы в экземплярах, но все экземпляры должны иметь ссылку на класс.

Например, если у вас есть простой класс, подобный этому:

class MyClass:
    def __init__(self):
        self.a = 1
        self.b = 2

    def __repr__(self):
        return f"{self.__class__.__name__}({self.a}, {self.b})"

instance_1 = MyClass()
instance_2 = MyClass()

Тогда в памяти это выглядит (очень упрощенно) так:

enter image description here

Идя глубже

Однако есть несколько вещей, которые важны при углублении в CPython:

  • Наличие словаря в качестве абстракции приводит к небольшим накладным расходам: вам нужна ссылка на словарь экземпляров (байты), и каждая запись в словаре хранит хеш (8 байт), указатель на ключ (8 байт) и указатель на сохраненный атрибут (еще 8 байтов). Кроме того, словари обычно перераспределяют, так что добавление другого атрибута не вызывает изменение размера словаря.
  • Python не имеет "типов значений", даже целое число будет экземпляром. Это означает, что вам не нужно 4 байта для хранения целого числа - Python требует (на моем компьютере) 24 байта для хранения целого числа 0 и не менее 28 байтов для хранения целых чисел, отличных от нуля. Однако ссылки на другие объекты просто требуют 8 байтов (указатель).
  • CPython использует подсчет ссылок, поэтому каждому экземпляру необходим счетчик ссылок (8 байт). Также большинство классов CPythons участвуют в циклическом сборщике мусора, который влечет за собой дополнительные 24 байта на экземпляр. В дополнение к этим классам, которые могут иметь слабые ссылки (большинство из них), также имеется поле __weakref__ (еще 8 байтов).

На этом этапе также необходимо указать, что CPython оптимизирует некоторые из этих «проблем»:

  • Python использует Словари совместного использования ключей , чтобы избежать некоторых накладных расходов памяти (хэш и ключ) словарей экземпляров.
  • Вы можете использовать __slots__ в классах, чтобы избежать __dict__ и __weakref__. Это может дать значительно меньший объем памяти на экземпляр.
  • Python интернирует некоторые значения, например, если вы создадите маленькое целое число, он не создаст новый целочисленный экземпляр, а вернет ссылку на уже существующий экземпляр.

Учитывая все это и то, что некоторые из этих моментов (особенно пункты об оптимизации) являются деталями реализации, трудно дать канонический ответ об эффективных требованиях к памяти для классов Python.

Сокращение объема памяти экземпляров

Однако, если вы хотите уменьшить объем памяти ваших экземпляров, обязательно попробуйте __slots__. У них есть недостатки, но если они к вам не относятся, это очень хороший способ уменьшить память.

class Slotted:
    __slots__ = ('a', 'b')
    def __init__(self):
        self.a = 1
        self.b = 1

Если этого недостаточно, и вы работаете с большим количеством «типов значений», вы также можете пойти дальше и создать классы расширений. Это классы, которые определены в C, но упакованы так, что вы можете использовать их в Python.

Для удобства я использую привязки IPython для Cython, чтобы симулировать класс расширения:

%load_ext cython
%%cython

cdef class Extensioned:
    cdef long long a
    cdef long long b

    def __init__(self):
        self.a = 1
        self.b = 1

Измерение использования памяти

Остается интересный вопрос после всей этой теории: как мы можем измерить память?

Я также использую обычный класс:

class Dicted:
    def __init__(self):
        self.a = 1
        self.b = 1

Я обычно использую psutil (даже если это прокси-метод) для измерения воздействия на память и просто измеряю, сколько памяти она использовала до и после. Измерения немного сдвинуты, потому что мне нужно как-то сохранить экземпляры в памяти, иначе память будет восстановлена ​​(немедленно). Кроме того, это только приблизительное значение, потому что Python на самом деле занимает немного памяти, особенно при большом количестве операций создания / удаления.


import os
import psutil
process = psutil.Process(os.getpid())

runs = 10
instances = 100_000

memory_dicted = [0] * runs
memory_slotted = [0] * runs
memory_extensioned = [0] * runs

for run_index in range(runs):
    for store, cls in [(memory_dicted, Dicted), (memory_slotted, Slotted), (memory_extensioned, Extensioned)]:
        before = process.memory_info().rss
        l = [cls() for _ in range(instances)]
        store[run_index] = process.memory_info().rss - before
        l.clear()  # reclaim memory for instances immediately

Память не будет точно одинаковой для каждого запуска, потому что Python повторно использует некоторую память, а иногда также сохраняет память для других целей, но это должно по крайней мере дать разумную подсказку:

>>> min(memory_dicted) / 1024**2, min(memory_slotted) / 1024**2, min(memory_extensioned) / 1024**2
(15.625, 5.3359375, 2.7265625)

Я использовал min здесь в основном потому, что мне было интересно, каков был минимум, и я разделил на 1024**2, чтобы преобразовать байты в мегабайты.

Резюме: Как и ожидалось, нормальному классу с dict потребуется больше памяти, чем классам со слотами, но классы расширения (если применимо и доступны) могут иметь еще меньший объем памяти.

Еще одним инструментом, который может быть очень удобен для измерения использования памяти, является memory_profiler, хотя я давно его не использовал.

9 голосов
/ 14 июня 2019

[edit] Нелегко получить точное измерение использования памяти процессом python; Не думаю, что мой ответ полностью отвечает на вопрос , но это один из подходов, который может быть полезен в некоторых случаях.

В большинстве подходов используются прокси-методы (createn объектов и оценки воздействия на системную память) и внешние библиотеки, пытающиеся обернуть эти методы.Например, потоки можно найти здесь , здесь и там [/ edit]

Вкл. cPython 3.7,минимальный размер экземпляра обычного класса составляет 56 байт;с __slots__ (без словаря), 16 байтов.

import sys

class A:
    pass

class B:
    __slots__ = ()
    pass

a = A()
b = B()
sys.getsizeof(a), sys.getsizeof(b)

output:

56, 16

Строки документов, переменные класса и аннотации типов не найдены на уровне экземпляра:

import sys

class A:
    """regular class"""
    a: int = 12

class B:
    """slotted class"""
    b: int = 12
    __slots__ = ()

a = A()
b = B()
sys.getsizeof(a), sys.getsizeof(b)

вывод:

56, 16

[править] Кроме того, см. @ LiuXiMin ответ для мера размера определения класса.[/ Править]

7 голосов
/ 17 июня 2019

Самый базовый объект в CPython - это просто ссылка типа и счетчик ссылок . Оба имеют размер слова (т. Е. 8 байт на 64-разрядном компьютере), поэтому минимальный размер экземпляра составляет 2 слова (т. Е. 16 байт на 64-разрядном компьютере).

>>> import sys
>>>
>>> class Minimal:
...      __slots__ = ()  # do not allow dynamic fields
...
>>> minimal = Minimal()
>>> sys.getsizeof(minimal)
16

Каждому экземпляру требуется место для __class__ и скрытый счетчик ссылок.


Ссылка на тип (примерно object.__class__) означает, что экземпляры извлекают содержимое из своего класса . Все, что вы определяете для класса, а не для экземпляра, не занимает места на экземпляр.

>>> class EmptyInstance:
...      __slots__ = ()  # do not allow dynamic fields
...      foo = 'bar'
...      def hello(self):
...          return "Hello World"
...
>>> empty_instance = EmptyInstance()
>>> sys.getsizeof(empty_instance)  # instance size is unchanged
16
>>> empty_instance.foo             # instance has access to class attributes
'bar'
>>> empty_instance.hello()         # methods are class attributes!
'Hello World'

Обратите внимание, что методы тоже являются функциями в классе . Выборка одного через экземпляр вызывает протокол дескриптора данных функции , чтобы создать временный объект метода путем частичной привязки экземпляра к функции. В результате методы не увеличивают размер экземпляра .

Экземплярам не требуется место для атрибутов класса, включая __doc__ и любые методы.


Единственное, что увеличивает размер экземпляров, это содержимое, хранящееся в экземпляре. Для этого есть три способа: __dict__, __slots__ и типы контейнеров . Все это хранит контент, назначенный экземпляру каким-либо образом.

  • По умолчанию экземпляры имеют поле __dict__ - ссылку на отображение, в котором хранятся атрибуты. Такие классы также имеют некоторые другие поля по умолчанию, например __weakref__.

    >>> class Dict:
    ...     # class scope
    ...     def __init__(self):
    ...         # instance scope - access via self
    ...         self.bar = 2                   # assign to instance
    ...
    >>> dict_instance = Dict()
    >>> dict_instance.foo = 1                  # assign to instance
    >>> sys.getsizeof(dict_instance)           # larger due to more references
    56
    >>> sys.getsizeof(dict_instance.__dict__)  # __dict__ takes up space as well!
    240
    >>> dict_instance.__dict__                 # __dict__ stores attribute names and values
    {'bar': 2, 'foo': 1}
    

    Каждый экземпляр, использующий __dict__, использует пространство для dict, имен и значений атрибутов.

  • Добавление поля __slots__ к классу создает экземпляры с фиксированным расположением данных. Это ограничивает разрешенные атрибуты теми, которые объявлены, но занимает мало места в экземпляре. Слоты __dict__ и __weakref__ создаются только по запросу.

    >>> class Slots:
    ...     __slots__ = ('foo',)  # request accessors for instance data
    ...     def __init__(self):
    ...         # instance scope - access via self
    ...         self.foo = 2
    ...
    >>> slots_instance = Slots()
    >>> sys.getsizeof(slots_instance)           # 40 + 8 * fields
    48
    >>> slots_instance.bar = 1
    AttributeError: 'Slots' object has no attribute 'bar'
    >>> del slots_instance.foo
    >>> sys.getsizeof(slots_instance)           # size is fixed
    48
    >>> Slots.foo                               # attribute interface is descriptor on class
    <member 'foo' of 'Slots' objects>
    

    Каждый экземпляр, использующий __slots__, использует пробел только для значений атрибута.

  • Наследование от типа контейнера, такого как list, dict или tuple, позволяет хранить элементы (self[0]) вместо атрибутов (self.a). При этом используется компактное внутреннее хранилище в дополнение к __dict__ или __slots__. Такие классы редко создаются вручную - часто используются такие помощники, как typing.NamedTuple.

    >>> from typing import NamedTuple
    >>>
    >>> class Named(NamedTuple):
    ...     foo: int
    ...
    >>> named_instance = Named(2)
    >>> sys.getsizeof(named_instance)
    56
    >>> named_instance.bar = 1
    AttributeError: 'Named' object has no attribute 'bar'
    >>> del named_instance.foo                  # behaviour inherited from container
    AttributeError: can't delete attribute
    >>> Named.foo                               # attribute interface is descriptor on class
    <property at 0x10bba3228>
    >>> Named.__len__                           # container interface/metadata such as length exists
    <slot wrapper '__len__' of 'tuple' objects>
    

    Каждый экземпляр производного контейнера ведет себя как базовый тип плюс потенциальный __slots__ или __dict__.

Наиболее легкие экземпляры используют __slots__ для хранения только значений атрибутов.


Обратите внимание, что часть служебных данных __dict__ обычно оптимизируется интерпретаторами Python. CPython способен совместно использовать ключи между экземплярами , что может значительно уменьшить размер для экземпляра . PyPy использует оптимизированное представление с общим ключом, которое полностью устраняет разницу между __dict__ и __slots__.

Невозможно точно измерить потребление памяти объектами во всех случаях, кроме самых тривиальных. Измерение размера изолированных объектов пропускает связанные структуры, такие как __dict__ с использованием памяти для и указателя на экземпляр и внешнего dict. Измерение групп объектов неправильно учитывает общие объекты (внутренние строки, маленькие целые числа, ...) и ленивые объекты (например, dict из __dict__ существует только при обращении). Обратите внимание, что PyPy не реализует sys.getsizeof , чтобы избежать его неправильного использования .

Для измерения потребления памяти следует использовать полное программное измерение. Например, можно использовать resource или psutils, чтобы получить собственное потребление памяти при порождении объектов .

Я создал один такой сценарий измерения для количество полей , количество экземпляров и вариант реализации . Показанные значения: байт / поле для счетчика экземпляров 1000000, в CPython 3.7.0 и PyPy3 3.6.1 / 7.1.1-beta0.

      # fields |     1 |     4 |     8 |    16 |    32 |    64 |
---------------+-------+-------+-------+-------+-------+-------+
python3: slots |  48.8 |  18.3 |  13.5 |  10.7 |   9.8 |   8.8 |
python3: dict  | 170.6 |  42.7 |  26.5 |  18.8 |  14.7 |  13.0 |
pypy3:   slots |  79.0 |  31.8 |  30.1 |  25.9 |  25.6 |  24.1 |
pypy3:   dict  |  79.2 |  31.9 |  29.9 |  27.2 |  24.9 |  25.0 |

Для CPython __slots__ экономит около 30% -50% памяти по сравнению с __dict__. Для PyPy потребление сопоставимо. Интересно, что PyPy хуже, чем CPython с __slots__, и остается стабильным при экстремальных значениях поля.

5 голосов
/ 14 июня 2019

Эффективно ли это и не нужно хранить что-либо, кроме определенных мной вещей, которые нужно хранить в каждом объекте?

Почти да, кроме некоторого определенного места. Класс в Python уже является экземпляром type, называемым метаклассом. Когда новый экземпляр объекта класса, custom stuff - это как раз те вещи в __init__. Атрибуты и методы, определенные в классе, не будут потратить больше места.

Что касается некоторого определенного места, просто обратитесь к ответу Маски Реблохона, очень хорошему и впечатляющему.

Может быть, я могу привести один простой, но иллюстративный пример:

class T(object):
    def a(self):
        print(self)
t = T()
t.a()
# output: <__main__.T object at 0x1060712e8>
T.a(t)
# output: <__main__.T object at 0x1060712e8>
# as you see, t.a() equals T.a(t)

import sys
sys.getsizeof(T)
# output: 1056
sys.getsizeof(T())
# output: 56
...