Сколько объектов выделяет Python, выполняя скрипты?

Original author: Artem Golubin
  • Translation
Некоторые Python программисты сильно удивляются, когда узнают сколько временных объектов интерпретатор питона выделяет во время работы простого скрипта.

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

./configure CFLAGS='-DCOUNT_ALLOCS' --with-pydebug 
make -s -j2

После компиляции, мы можем открыть интерактивный REPL и проверить статистику:

>>> import sys
>>> sys.getcounts()
[('iterator', 7, 7, 4), ('functools._lru_cache_wrapper', 1, 0, 1), ('re.Match', 2, 2, 1),
('re.Pattern', 3, 2, 1), ('SubPattern', 10, 10, 8), ('Pattern', 3, 3, 1),
('IndexError', 4, 4, 1), ('Tokenizer', 3, 3, 1), ('odict_keys', 1, 1, 1),
('odict_iterator', 18, 18, 1), ('odict_items', 17, 17, 1), ('RegexFlag', 18, 8, 10),
('operator.itemgetter', 4, 0, 4), ('PyCapsule', 1, 1, 1), ('Repr', 1, 0, 1),
('_NamedIntConstant', 74, 0, 74), ('collections.OrderedDict', 5, 0, 5),
('EnumMeta', 5, 0, 5), ('DynamicClassAttribute', 2, 0, 2), ('_EnumDict', 5, 5, 1),
('TypeError', 1, 1, 1), ('method-wrapper', 365, 365, 2), ('_C', 1, 1, 1),
('symtable entry', 5, 5, 2), ('OSError', 1, 1, 1), ('Completer', 1, 0, 1),
('ExtensionFileLoader', 2, 0, 2), ('ModuleNotFoundError', 2, 2, 1),
('_Helper', 1, 0, 1), ('_Printer', 3, 0, 3), ('Quitter', 2, 0, 2),
('enumerate', 5, 5, 1), ('_io.IncrementalNewlineDecoder', 1, 1, 1),
('map', 25, 25, 1), ('_Environ', 2, 0, 2), ('async_generator', 2, 1, 1),
('coroutine', 2, 2, 1), ('zip', 1, 1, 1), ('longrange_iterator', 1, 1, 1),
('range_iterator', 7, 7, 1), ('range', 14, 14, 2), ('list_reverseiterator', 2, 2, 1),
('dict_valueiterator', 1, 1, 1), ('dict_values', 2, 2, 1), ('dict_keyiterator', 25, 25, 1),
('dict_keys', 5, 5, 1), ('bytearray_iterator', 1, 1, 1), ('bytearray', 4, 4, 1),
('bytes_iterator', 2, 2, 1), ('IncrementalEncoder', 2, 0, 2), ('_io.BufferedWriter', 2, 0, 2),
('IncrementalDecoder', 2, 1, 2), ('_io.TextIOWrapper', 4, 1, 4), ('_io.BufferedReader', 2, 1, 2),
('_abc_data', 39, 0, 39), ('mappingproxy', 199, 199, 1), ('ABCMeta', 39, 0, 39),
('CodecInfo', 1, 0, 1), ('str_iterator', 7, 7, 1), ('memoryview', 60, 60, 2),
('managedbuffer', 31, 31, 1), ('slice', 589, 589, 1), ('_io.FileIO', 33, 30, 5),
('SourceFileLoader', 29, 0, 29), ('set', 166, 101, 80), ('StopIteration', 33, 33, 1),
('FileFinder', 11, 0, 11), ('os.stat_result', 145, 145, 1), ('ImportError', 2, 2, 1),
('FileNotFoundError', 10, 10, 1), ('ZipImportError', 12, 12, 1), ('zipimport.zipimporter', 12, 12, 1),
('NameError', 4, 4, 1), ('set_iterator', 46, 46, 1), ('frozenset', 50, 0, 50), ('_ImportLockContext', 113, 113, 1),
('list_iterator', 305, 305, 5), ('_thread.lock', 92, 92, 10), ('_ModuleLock', 46, 46, 5), ('KeyError', 67, 67, 2),
('_ModuleLockManager', 46, 46, 5), ('generator', 125, 125, 1), ('_installed_safely', 52, 52, 5),
('method', 1095, 1093, 14), ('ModuleSpec', 58, 4, 54), ('AttributeError', 22, 22, 1),
('traceback', 154, 154, 3), ('dict_itemiterator', 45, 45, 1), ('dict_items', 46, 46, 1),
('object', 8, 1, 7), ('tuple_iterator', 631, 631, 3), ('cell', 71, 31, 42),
('classmethod', 58, 0, 58), ('property', 18, 2, 16), ('super', 360, 360, 1),
('type', 78, 3, 75), ('function', 1705, 785, 922), ('frame', 5442, 5440, 36),
('code', 1280, 276, 1063), ('bytes', 2999, 965, 2154), ('Token.MISSING', 1, 0, 1),
('stderrprinter', 1, 1, 1), ('MemoryError', 16, 16, 16), ('sys.thread_info', 1, 0, 1),
('sys.flags', 2, 0, 2), ('types.SimpleNamespace', 1, 0, 1), ('sys.version_info', 1, 0, 1),
('sys.hash_info', 1, 0, 1), ('sys.int_info', 1, 0, 1), ('float', 584, 569, 20),
('sys.float_info', 1, 0, 1), ('module', 56, 0, 56), ('staticmethod', 16, 0, 16),
('weakref', 505, 82, 426), ('int', 3540, 2775, 766), ('member_descriptor', 246, 10, 239),
('list', 992, 919, 85), ('getset_descriptor', 240, 4, 240), ('classmethod_descriptor', 12, 0, 12),
('method_descriptor', 678, 0, 678), ('builtin_function_or_method', 1796, 1151, 651), ('wrapper_descriptor', 1031, 5, 1026),
('str', 16156, 9272, 6950), ('dict', 1696, 900, 810), ('tuple', 10367, 6110, 4337)]

Сделаем вывод более читабельным:

def print_allocations(top_k=None):
    allocs = sys.getcounts()
    if top_k:
        allocs = sorted(allocs, key=lambda tup: tup[1], reverse=True)[0:top_k]

    for obj in allocs:
        alive = obj[1]-obj[2]
        print("Type {},  allocs: {}, deallocs: {}, max: {}, alive: {}".format(*obj,alive))

>>> print_allocations(10)
Type str,  allocs: 17328, deallocs: 10312, max: 7016, alive: 7016
Type tuple,  allocs: 10550, deallocs: 6161, max: 4389, alive: 4389
Type frame,  allocs: 5445, deallocs: 5442, max: 36, alive: 3
Type int,  allocs: 3988, deallocs: 3175, max: 813, alive: 813
Type bytes,  allocs: 3031, deallocs: 1044, max: 2154, alive: 1987
Type builtin_function_or_method,  allocs: 1809, deallocs: 1164, max: 651, alive: 645
Type dict,  allocs: 1726, deallocs: 930, max: 815, alive: 796
Type function,  allocs: 1706, deallocs: 811, max: 922, alive: 895
Type code,  allocs: 1284, deallocs: 304, max: 1063, alive: 980
Type method,  allocs: 1095, deallocs: 1093, max: 14, alive: 2

Где:

  • allocs — сколько объектов было выделено с момента старта интерпретатора
  • deallocs — сколько объектов было удалено (вручную или автоматически)
  • alive — количество живых (текущих) объектов (allocs — deallocs)
  • max — максимальное количество живых объектов с момента старта интерпретатора

Как вы можете видеть, пустой Python REPL успел выделить 17 328 строк and 10 550 кортежей. Это какое-то безумное количество объектов! Здесь нужно иметь в виду, что для работы REPL, Python автоматически импортирует дополнительные модули, которые не импортируются в случае с пустыми скриптами.

Теперь давайте протестируем «Hello, World» на flask:

import sys

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    print_allocations(15)
    return 'Hello, World!'

./python -m flask run
ab -n 100 http://127.0.0.1:5000/

После отправки 100 HTTP запросов на наш сервер статистика выглядит так:

Type str,  allocs: 192649, deallocs: 138892, max: 54320, alive: 53757
Type frame,  allocs: 191752, deallocs: 191714, max: 158, alive: 38
Type tuple,  allocs: 183474, deallocs: 150069, max: 33581, alive: 33405
Type int,  allocs: 85154, deallocs: 81100, max: 4115, alive: 4054
Type bytes,  allocs: 31671, deallocs: 14331, max: 17381, alive: 17340
Type list,  allocs: 29846, deallocs: 27541, max: 2415, alive: 2305
Type builtin_function_or_method,  allocs: 28525, deallocs: 27572, max: 957, alive: 953
Type dict,  allocs: 19900, deallocs: 14800, max: 5280, alive: 5100
Type method,  allocs: 15170, deallocs: 15105, max: 74, alive: 65
Type function,  allocs: 14761, deallocs: 7086, max: 7711, alive: 7675
Type slice,  allocs: 12521, deallocs: 12521, max: 1, alive: 0
Type list_iterator,  allocs: 10795, deallocs: 10795, max: 35, alive: 0
Type code,  allocs: 9849, deallocs: 1749, max: 8107, alive: 8100
Type tuple_iterator,  allocs: 8938, deallocs: 8938, max: 4, alive: 0
Type float,  allocs: 6033, deallocs: 5889, max: 152, alive: 144

Как можно видеть, flask выделил 847 261 объектов с момента старта интерпретатора. Большая часть из них была временной (714 336) и удалена как только они больше были не нужны. Остальные объекты (132 925) по прежнему находятся в памяти.

Фреймы и code объекты


В примере выше можно встретить множество frame и code объектов. Зачем они нужны?

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

Откуда берутся все эти объекты?


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

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

Для того, чтобы Python быстро выделял большое количество объектов, в нём используется большая и многослойная система, которая оптимизирует выделение объектов в памяти.

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

P.S.: Я являюсь автором этой статьи, можете задавать любые вопросы.

Similar posts

Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 7

    –1
    К чему эта статья? Что хотел сказать автор?
      +2
      Это просто занимательная статистика, не более, некоторые мои коллеги даже близко не представляли количество выделяемых объектов в Python. Новички в Python, которые пришли из низкоуровневых языков по типу C/C++ тоже удивляются таким цифрам.

      Ну и ещё один пример, почему динамические языки могут быть медленными.
        0
        Спасибо!
        Я в шоке! Теперь понятно почему «Здарова Мир» так долго выводится.
          0
          Не вижу, чтобы где-то доказывалось замедление работы именно из-за количества выделяемых объектов. Такая статистика сама по себе бессмысленна.
        0
        Замечательные интроспективные возможности Питона наводят на интересную мысль.
        Представьте, что мы написали некий инструмент, который бы визуализировал всё, что происходит под капотом интерпретатора во время выполнения программы. Пусть каждый объект отображается кубиком,… пусть стили оформления отличаются для разных категорий типов объектов. Ссылки сильные и слабые обозначим стрелками. Байткод у нас тоже где-то в этом гигантском переплетении объектов будет размещён. Точку выполнения обозначим рамкой со шлейфом, которая будет скользить и перепрыгивать от команды к команде, заставляя светиться представления функций, внутри которых сейчас курсор исполнения команд.
        Давайте, чтобы совсем уж не запутаться и не попасть в петлю бесконечных рекуррентных вызовов (ведь не получится за конечное время визуализировать код, который визуализирует) будем где-то держать черный список модулей, визуализация для которых отключена для простоты и понятности.

        Не уверен возможна ли была бы такая штука, но зрелище было бы завораживающее своей сложностью и масштабом… и бессмысленностью=)
          0
          Если вы не видели картинок от RunSnakeRun и его преемника SnakeViz, то не поленитесь погуглить. Профайлинг кода превращается в завораживающее путешествие, а со стороны — в магию.
            0
            Спасибо. Не знал об этом инструменте.

        Only users with full accounts can post comments. Log in, please.