Pull to refresh

Comments 25

Как-то рассуждая о проблемах оптимизации использования памяти и времени выполнения я услышал на мой взгляд классный вывод:


Сегодня проблемы памяти и времени не существует. Существует проблема денег.

Ну не всегда это правильно, существуют моменты пикового потребления памяти, например до 10..20гб, а в остальное время, 99.9%, приложение живет в скромных 100..200мб.
Плюс, не надо забывать, что сначала к нам приходит 10 человек в минуту, а через неделю 100 и так далее. Можно и устать масштабироваться :)

Recordclass: мутируемый namedtuple без GC


А как в данном случае чистить память? Или ждать конца работы программы?

Здесь срабатывает механизм подсчета ссылок. Как только количество ссылок станет равным нулю, объект сразу утилизируется. Предполагается, что циклические ссылки на этот объект не возникают.

Можно обосновать минус? Что в ответе не верно?

Минус ставил не я, но задам вопрос. Подсчет ссылок это же и есть GC?

И да, и нет. Подсчет ссылок — это часть GC. Т.к. GC брлее широкое понятие, включающее в себе определение, например, кластеров объектов, ссылающихся друг на друга (но не имеющих ссылки извне), и, например, релокации объектов в памяти для уменьшения ее фрагментации

Нет, это разные вещи. С разным механизмом работы, разными возможностями и разным влиянием на производительность.


Простым подсчетом ссылок не определишь, что набор объектов с зацикленными ссылками стал недостижимым (напр. все двусвязные списки из >1 ноды будут утекать. Т.к. в структуре A ⇆ B и на A и на B всегда есть как минимум одна ссылка. Даже когда кроме их самих никто на них не ссылается).


У GC принцип работы другой — он просто обходит всю компоненту связности графа объектов, начиная с "корневого" объекта, и затем убивает все объекты, которые во время этого обхода оказались недостижимыми (на самом деле обходится не весь граф, есть разделение поколений объектов и другие оптимизации, но сам принцип остается прежним — обходим (под-)граф и удаляем все, к чему не дошли).


В Python используются оба этих механизма.

Не совсем, например Appleвские языки: Objectiv-C, Swift не имеют GC, но имеют автоматическую сборку мусора.

Вы меня запутали ) GC — garbage collector, то есть сборка мусора

Нет, GC- сборщик мусора, то есть компонент рантайма, который периодически выходит на уборку.

При подсчете ссылок, не нужный объект умирает в момент обнуления счетчика, тогда как GC убивает в непредсказуемый момент времени после окончания использования объекта.

Вероятно опечатка) В эппловких языках нет GC, но есть подсчет ссылок. А скажем, в джаве есть GC, а подсчета ссылок нет.


В Python есть и то и другое. Можно рассматривать подсчет ссылок как одну из оптимизаций GC, но в общем случае это независимые вещи, которые прекрасно работают отдельно друг от друга, что видно из примеров выше.

GC — garbage collector — это модуль рантайма, т.к. сборщик мусора
GC — как garbage collection — это процесс (в смысле как явление, а не в смысле потока рантайма), который он обеспечивает.

В Python есть механизм подсчёта ссылок — это основной способ управления памятью и жизнью объектов. И есть GC, который основан на поколениях: https://docs.python.org/3/library/gc.html


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


А ещё в Python есть модуль weakref для создания слабых ссылок, но не уверен, что этот механизм будет работать со штуками из recordclass.

Можно создавать классы с поддержкой weakref, но без участия в механизме циклической сборки мусора:


 @clsconfig(weakref=True)
  class Point(dataobject):
      x:int
      y:int

или


Point = make_dataclass('Point', ('x','y'), use_weakref=True)

В Python есть основной механизм подсчета ссылок на объект. Если объект таков, что циклические ссылки на него в графе зависимостей между объектами не возникают, то после того, как счетчик ссылок (находится в заголовке объекта PyObject_HEAD) обнуляется, что означает что объект более никем не используется, то автоматически утилизируется из памяти. Тут правда есть нюансы, связанные с распределителем памяти объектов в Python, но оставим это за скобками. Дополнительно есть механизм для утилизации для объектов, в которым возможны циклические ссылки. В объекте-типе для классов установлен специальный флаг, который сигнализирует, что объект должен быть утилизирован через механизм циклической сборки мусора, который включает в себя обход таких объектов с целью ликвидации циклов в графе зависимостей между объектами. Для простых объектов типа str, long, date/time такой флаг не выставлен и они утилизируются через механизм подсчета ссылок. Контейнерные типы list, dict, любой класс созданный через обычное определение class, и даже немутируемый tuple имеют такой флаг. Однако не всегда маркирование класса автоматически, как потенциальный для возникновения циклических ссылок, оправдано. Например, объекты, которые представляют простые структуры, которые в графе зависимостей между объектами по-существу являются терминальными. Корректное использование таких классов не приводит к возникновению циклических ссылок. Поэтому здесь механизм циклической сборки мусора избыточен и может быть и может быть исключен.


Задавшему вопрос отдельная благодарность за возможность прояснить ситуацию.

Странное ощущение, что уже видел эту статью
у меня только один вопрос: а где array.array или ctypes.Structure? же реализуют идею data locality, т.е для единичного объекта может и не шибко эффективно как и половина способов описанных автором, а вот для пачки (большой пачки) выгода может быть очень даже заметная.

Вы правы, это вышло за скобки. Я не преследовал цель сделать полный обзор. Но отметил наиболее часто используемые. Что касается array.array для большого числа значений, выигрыш по памяти есть, так же как и в случае с numpy. Но остается нагрузка в связи с необходимостью преобразования С-шных типов в Python-овские и наоборот. Если выигрыш в памяти важнее, чем потеря производительности в связи с boxing/unboxing, то да действительно это работает. Что касается ctypes.Structure, у меня нет опыта работы с ctypes. Однако, исторически ctypes создавался для того, чтобы обеспечивать доступ к структурам, которые изначально живут в бинарных модулях (dll/so), которые создавались при помощи C/C++ или следовали соответствующим конвенциям.
Проблема boxing/unboxing здесь остается.


В статье основной акцент старался сделать на том, как в "чистом" Python добиться уменьшения потребления памяти при создании объектов, которые тоже состоят из Python-овских объектов.

Из за особенностей объектов в python sys.getsizeof() выдает результат только для верхнего уровня данных не переходя по ссылкам. Есть удобный инструмент pympler. Он циклически раскручивает объект целиком.
import sys
from pympler import asizeof

class A:
    def __init__(self):
        self.x = 1

class B:
    def __init__(self):
        self.y = "hello world"
        self.z = {'a': 1, 'b': 2}

a = A()
b = B()

print(asizeof.asizeof(a))  # 256
print(sys.getsizeof(a))  # 56

print(asizeof.asizeof(b))  # 760
print(sys.getsizeof(b))  # 56

Для экземпляров классов, порожденных recordclass и унаследованных от dataobject pympler не может вычислить правильный размер экземпляра. Похоже, что для экземпляров классов со __slots__ тоже почему-то дает завышенное значение.

Для того чтобы понять почему (см. https://raw.githubusercontent.com/pympler/pympler/master/pympler/asizeof.py):


Note, objects like ``namedtuples``, ``closure``, and NumPy data
``arange``, ``array``, ``matrix``, etc. are only handled by recent
versions of this module.  Sizing of ``__slots__`` has been incorrect
in versions before this one.  Also, property ``Asizer.duplicate`` gave
incorrect values before this release.  Several other properties
have been added to the ``Asizer`` class and the ``print_summary``
method has been updated.

Поэтому для того, чтобы pympler мог давать верный размер для объектов, порожденных подклассами dataobject, его нужно сначала "научить" )

@intellimath, спасибо за интересную статью. С точки зрения экономии памяти выбор из предложенных вами инструментов понятен. Может быть вы из своего опыта можете подсказать, как эти инструменты соотносятся с dict() по скорости доступа к значениям?

В задаче, которую я решаю, ключевой приоритет – максимально быстрое выполненеи программы. dict() с доступом к значениям по ключам дает нужную мне скорость, но из-за объема словаря занимает 80% оперативной памяти.

Какой-то из предложенных вами инструментов может помочь снизить объем памяти хотя бы до 50% (т.е. раза в полтора по сравнению с dict), но при этом не потерять в скорости работы?

Чисто по вашему описанию slots выглядит как подходящий вариант. Вопрос в том не будет ли потери в скорости, если заменить x[key] на x.key

dict имеет наибольшую скорость доступа по ключу. До python3.11 класс со __slots__ имеет меньшую скорость доступа. С python3.11 появилась оптимизация доступа для экземпляров со __slots__, которая делает скорость доступа очень высокой. recordclass.recordclass/dataobject имеет более высокую скорость создания экземпляра, и высокую скорость доступа. В стандартном варианте скорость создания экземпляра со __slots__ невысокая. Для сравнения можно посмотреть таблицу сравнения производительности в README (https://github.com/intellimath/recordclass/blob/main/README.md)

Спасибо за ответ и за ссылку. Полезная таблица, хорошо иллюстрирует положение дел.

Sign up to leave a comment.

Articles