Архив метки: Python

Удаление объектов сборщиком мусора

На Хабре появилась толковая статьяо неприятностях, которые может доставить Garbage Collector.

Отмечу актуальность описания проблемы и полную корректность решения для tornado.

Однако не могу не обратить внимание на слабое освещение того, как именно работает сборщик мусора в CPython.

Почему при работающем garbage collector появляется так много неудаляемых объектов?

Немного теории.

Сборщик мусора имеет три поколения (счёт начинается с нуля). При создании объекта он попадает в нулевое поколение.

У каждого поколения есть счётчик и порог. Работает эта пара так:

  • При добавлении объекта в поколение счётчик увеличивается.
  • При выбывании из поколения счётчик уменьшается.
  • Когда счётчик превысит пороговое значение — по всем объектам из поколения пройдётся сборщик мусора. Кого найдёт — удалит.
  • Все выжившие в поколении объекты перемещаются в следующее (из нулевого в первое, из первого во второе). Из второго поколения объекты никуда не попадают и остаются там навечно.
  • Перемещённые в следующее поколение объекты меняют соответствующий счетчик, и операция может повториться уже для следующего поколения.
  • Счётчик текущего поколения сбрасывается.

Объекты, подлежащие уничтожению но имеющие переопределённый метод __del__ не могут быть собраны. Причина проста: эти объекты могут ссылаться друг на друга.

Python не способен определить безопасный порядок вызова __del__. Если вызывать деструкторы в произвольном порядке, то можно получить ситуацию вида:

  • Деструктор объекта a для работы требует объект b.
  • Последний в своём деструкторе обращается к объекту a.
  • Если вызовем __del__ у a, то деструктор b не сможет отработать нормально. Ссылка на a будет иметь значение None.

Чтобы не заставлять программиста корректно разрешать такие ситуации было принято решение не уничтожать подобные объекты а просто перемещать их в gc.garbage — и дальше программист пусть сам разбирается что делать с этим мусором.

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

Перейдём к практической части.

Пример

Для иллюстрации рассмотрим немного изменённый пример из приведенной в самом начале статьи.

Имеем классическую древовидную структуру:

class Node(object):
parent = None

def __init__(self, *children):
self.children = list(children)
for node in self.children:
node.parent = self

@classmethod
def tree(cls, depth=1, numchildren=1):
if depth == 0:
return []
return [cls(*cls.tree(depth-1, numchildren))
for _ in range(numchildren)]

Родитель и потомки напрямую связаны друг с другом циклической связью. Метод tree создает дерево нужной глубины.

Добавляем garbage collection hook для того чтобы увидеть когда срабатывает сборщик мусора и сколько объектов он уничтожает:

import gc

def gc_cb(phase, info):
if not info['collected'] and not info['uncollectable']:
return
print("{0}:t{1[generation]}t{1[collected]}t{1[uncollectable]}".format(
phase, info))

gc.callbacks.append(gc_cb)

Наконец, делаем много-много наших деревьев и смотрим как они разрушаются:

for n in range(20):
for _ in range(n):
Node.tree(depth=5, numchildren=6)

Пороги стоят стандартные:

>>> gc.get_threshold()
(700, 10, 10)

700 объектов в нулевом поколении и по 10 в первом и во втором.

Анализ

Теперь о том, почему образуется столько мусора.

Пример напечатает что-то вроде такого (вырезка из очень длинного результата):

...
stop: 1, 4665, 0
stop: 2, 79305, 0
stop: 1, 4665, 0
stop: 2, 79305, 0
stop: 1, 4665, 0
stop: 1, 4665, 0
stop: 1, 4665, 0
stop: 2, 97965, 0
stop: 1, 4665, 0
stop: 2, 79305, 0
stop: 1, 4665, 0
...

За один вызов Node.tree(depth=5, numchildren=6) создается 9330 тесно связанных объектов, которые нельзя разрушить в 0 поколении (помним, что порог 700). Значит они попадают в первое, а большая часть даже во второе поколение (9330>700*10). Наконец все 9330 объекта созданы, можно разрушать.

На уменьшении счётчиков ссылок на объекты ничего убрать не получится. Поэтому ждём, когда опять превысим порог в 700 (на следующем вызове Node.tree, конечно).

Собираем нулевое поколение (оно оказывается заполнено свежими данными и поживиться почти ничем не удаётся).

А сборщик мусора для поколения 1 вызовется только если туда попадут как минимум 10 объектов из поколения 0.

Хорошо, мы добрались до сбора в 1 поколении. Часть циклов мож
но уничтожить сразу (два поколения для анализа лучше одного), некоторые переправляются в поколение 2. В котором сборщик запускается тоже если в свою очередь превысили порог.

Что случается ещё реже и таким образом наши объекты накапливаются во втором поколении. Когда сборщик мусора доходит до него, то всё чистит.

Проблема в том, что до последнего поколения дело доходит относительно редко.

В результате имеем не слишком типичный для сборщика мусора случай.

Чиним

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

Разрушать ссылки вручную через вызов del или присваивания Noneочень неудобно, но есть и другой способ.

Воспользуемся слабыми ссылками на родителя:

import weakref

class Node(object):
parent = None

def __init__(self, *children):
self.children = list(children)
for node in self.children:
node.parent = weakref.proxy(self)

Я предпочитаю weakref.ref как дающий больший контроль (всегда можно добавить свойство):

class Node(object):
_parent = None

def __init__(self, *children):
self.children = list(children)
for node in self.children:
node._parent = weakref.ref(self)

@property
def parent(self):
if self._parent is None:
return None
else:
return self._parent()

В любом варианте дело до сборщика мусора не дойдёт и объекты будут уничтожены сразу как только перестанут быть нужны.

Если вариант со слабыми ссылками почему-то не проходит можно просто увеличить пороги. У нас создаётся за раз 9330 объектов? Поставим порог для первого поколения в 10000.

gc.set_threshold(10000, 100, 100)

Результат выглядит куда лучше:

...
stop: 0, 4665, 0
stop: 0, 4665, 0
stop: 0, 4665, 0
stop: 0, 4665, 0
stop: 0, 4665, 0
stop: 1, 919005, 0
stop: 0, 4665, 0
stop: 0, 4665, 0
stop: 0, 4665, 0
stop: 0, 4665, 0
...

Как видим сборщик мусора уничтожает почти всё на первом проходе, и лишь иногда требуется второй. Правда, смущает цифра 919005.

Именно потому что не всё прибивается на первом проходе, а второй наступает нескоро.

Уменьшаем второй порог:

gc.set_threshold(10000, 10, 10)

Ага, теперь всё красиво:

...
stop: 1, 83970, 0
stop: 0, 9330, 0
stop: 0, 4665, 0
stop: 0, 4665, 0
stop: 1, 74640, 0
stop: 0, 4665, 0
stop: 0, 4665, 0
stop: 1, 102630, 0
stop: 0, 4665, 0
stop: 0, 4665, 0
...

Выводы

В результате всё просто. Используем слабые ссылки. Если это по каким-то причинам невозможно — поднимаем пороги.

Но при этом нужно помнить, что сборщик мусора будет запускаться реже.

Установка порогов в слишком большое значение способно в нашем случае съесть память не менее успешно, чем если бы эти значения оставались установленными по умолчанию.

Автор: Andrew Svetlov

Начало курсов «Learn Python»

Занятия начинаются в эту субботу, 29 июня 2013.

Записавшимся должно прийти на почту письмо с местом, временем и прочими деталями.

Автор: Andrew Svetlov

Алгоритм Бойера — Мура

Алгоритм поиска строки Бойера — Мура считается наиболее быстрым среди алгоритмов общего назначения, предназначенных для поиска подстроки в строке. Был разработан Робертом Бойером и Джеем Муром в 1977 году. Преимущество этого алгоритма в том, что ценой некоторого количества предварительных вычислений над шаблоном (но не над строкой, в которой ведётся поиск) шаблон сравнивается с исходным текстом не во всех позициях — часть проверок пропускаются как заведомо не дающие результата.
Читать →

Новый набор на курсы Learn Python

Объявляем набор участников на наши новые потоки курсов о Python: Aсинхронное программирование, Создание эффективных веб-приложений и Оптимизация Python кода. В этот раз потоки будут длиться по 6 занятий. Планируемая дата начала занятий: июнь 2013 года.

Асинхронное программирование

Сетевые библиотеки, например twisted, tornado, gevent, tulip — при кажущейся разнице в подходах имеют очень похожее ядро, называемое reactor, io loop, hub, event loop соответственно. Именно созданием этого ядра с нуля своими руками мы и займемся.

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

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

Потребуется Python 3.3. Хотя код мало зависит от версии языка, всё же в Python 3.3 появились некоторые удобные штуки которыми мы воспользуемся.

Операционная система: Linux, MacOS X или FreeBSD на выбор. Если есть сильное желание писать на Windows — тоже можно.

Коротко о чём будут занятия:

  • Написание примитивного клиент-серверного кода на потоках.
  • Объяснение почему производительные программы такой подход не используют. Нужно делать на epoll или kqueue, в крайнем случае select. Создание своего event loop. Сначала для обработки отложенных событий. Что это такое и какой должен быть интерфейс — расскажу по ходу дела.
  • Описание того, как работает select/epoll/kqueue. Добавляем к event loop работу с TCP сокетами, основанную на обратных вызовах. Правильная обработка ошибок.
  • Добавляем понятия транспорта-протокола.
  • Строим поверх этого удобный интерфейс для пользовательского кода. На yeild from или greenlets — по желанию.
  • Окончательный разбор результатов, ответы на возникшие вопросы.

Получившийся код в целом будет в основе следовать дизайну tulip в сильно упрощённом виде.

Курс состоит из шести занятий. Лектор: Андрей Светлов

Каждое занятие длится 2 часа.

Стоимость занятия: 300 грн.

Создание эффективных web-приложений

В 2013м году никого не удивишь веб-приложением, построенным при помощи Django, Pyramid или даже Flask. Однако куда сложнее удивить грамотным и эффективным web-приложением, способным одинаково успешно справлятся с нагрузкой реального мира и оставаться простым и легким для разработки.

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

На протяжении всего курса мы будем создавать веб-приложение, ориентированное на работу на ARM микро-компьютере Raspberry PI.

Требования к участникам: опыт в создании сайтов или проектов при помощи Python и популярных фреймворков. Учить создавать сайты с нуля не буду, буду помогать перейти на новый уровень и избегать довольно популярных и тем не менее назойливых ошибок.

Краткое содержания курса:

  • Архитектура высоконагруженного приложения, разделение проекта на бэкенд и фронтэнд, удаленное выполнение задач
  • Оптимизация архитектуры и оптимизация кода, что идет за чем
  • Тестирование как двигатель разработки, а не наоборот
  • Непрерывная интеграция и непрерывный деплоймент, сравнение мест для развертки проектов
  • Откуда берутся основные ошибки веб-приложений, зачем мы наступаем на одни и те же грабли

Курс состоит из шести занятий. Лектор: Игорь Давыденко

Каждое занятие длится 2 часа с перерывом в 15 минут.

Стоимость занятия: 200 грн, стоимость полного курса: 1000 грн.

Оптимизация Python кода

Чтобы делать высокоэффективный код нужно уметь пользоваться профайлером, читать байткод, выполнять алгоритмическую оптимизацию и писать Python C Extensions если алгоритмически выжать уже ничего не получается.

Всем этим мы и займемся.

Требования к участникам: уметь программировать на Python и C. Последнее очень желательно хотя бы на уровне остаточных знаний из институтского курса — половина рассматриваемого кода будет на С.

Python 3.3, операционная система любая.

Краткое со
держание курса:

  • Профилирование через cProfile и timeit, анализ измеренных результатов. Рассматриваем из чего состоит функция с точки зрения Python и добираемся до байткода. Несколько простых вариантов оптимизации.
  • Создаём простейший модуль Python C Extension.
  • Учимся делать Python классы на C.
  • Теперь пишем на Cython и радуемся как легко всё получается. В нагрузку ctypes.
  • Показываю, как устроена виртуальная CPython машина изнутри. Интерпретаторы, потоки, стек. GIL. Как PyEval_EvalFrameEx исполняет байткод.

Курс состоит из пяти занятий. Лектор: Андрей Светлов.

Каждое занятие длится 2 часа.

Стоимость занятия: 300 грн.

UPD. Для тех кто ещё не понял: online версии не будет. Ни в каком виде.

Автор: Andrew Svetlov

Задача о путешествии шахматного коня

Задача о ходе коня — задача о нахождении маршрута шахматного коня, проходящего через все поля доски по одному разу.

Эта задача известна по крайней мере с XVIII века. Леонард Эйлер посвятил ей большую работу «Решение одного любопытного вопроса, который, кажется, не подчиняется никакому исследованию» (датируется 26 апреля 1757 года).

Читать →

Pythons Innards

Перечитываю снова Pythons Innards (Yaniv Aknin).

Это — серия из нескольких статей о том, как устроен CPython изнутри.

  • Интерпретатор байткода
  • Что из себя представляет этот самый байткод
  • Что такое стек в понятии CPython, и как оно работает
  • Как устроены дескрипторы, слоты и классы на нижнем уровне
  • Пространства имен (namespaces)

Для «простых программистов, работающих работу» — наверное, ничего интересного.
Тем, кто желает разобраться в Питоне «до последнего байта» — очень рекомендую.

Вдобавок очень хочу посоветовать Ely Bendersky с его Python Internals. Эта серия статей тоже посвящена «внутреннему устройству» и прекрасно сочетается с тестами Yaniv Aknin.

  • Как создаётся объект (в деталях)
  • Что такое class и чем отличается от type
  • Как именно происходит вызов callable
  • И т.д. (symbol tables, например)

На самом деле подобного рода информации очень немного. Документация хорошо описывает CPython C API но не рассказывает о деталях, о том как это всё работает.

У меня до C кода дело доходило разве до обсуждения реализации GIL насколько я помню.

Если есть еще интересные статьи по внутреннему устройству — пишите в комментариях, я добавлю сюда. Наверняка что-то запамятовал, но в целом тема раскрыта очень скудно.

Так что если хотите узнать «как оно работает на самом деле» — читайте  статьи по ссылкам.

UPD.
По устройству типов данных:
Строки
Целые числа
Списки
Словари (для версий Python <3.3)Лекция Larry Hastings (release manager для Python 3.4, между прочим) с US PyCon 2012 на рассматриваемую тему.

Автор: Andrew Svetlov