It can be very surprising to see how many temporarily allocates while executing simple scripts. In fact, Python provides a way to check it.

To do so, we need to compile a standard CPython interpreter with additional debug flags:

./configure CFLAGS='-DCOUNT_ALLOCS' --with-pydebug 
make -s -j2
>>> 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)]

As you can see, an empty Python REPL has allocated 17 328 strings and 10 550 tuples. That’s an insane amount of allocations! Keep in mind, that unlike regular Python script, REPL imports additional modules which implement its features.

With all the requests, a Python interpreter running a simple flask application has allocated 847 261 objects in total. Most of them were temporal (714 336) and deallocated after they are no longer needed. The rest (132 925) are still alive.

There are a lot of code and frame objects in the example above. Why do we need them?

In short, each code object stores a block of compiled code, furthermore frame object represents a call stack. In Python, the most popular block is a function. Every function definition creates a code object, and every function execution requires a unique frame object, where Python stores local variables. Apart from local variables, each frame object may tens of auxiliary objects.

Python has a very dynamic nature which comes at a cost. In order to support many features at runtime, it allocates a lot of auxiliary objects.

For example, I found out that a simple function definition allocates at least five dictionaries, five tuples, and four lists that live until the end of a Python process. In turn, all these objects allocate its members, e.g., integers, floats, and strings. An ordinary class definition may allocate hundreds of container objects. Unfortunately, it’s hard to tell precise numbers since I didn’t find an easy way to measure it automatically.

To speed up and provide fast performance when allocating objects, Python has a variety of optimizations that improve object allocation.

Sometimes it’s good to know how many unnecessary (for a Python user) but interesting details Python hides from us.



Source link

LEAVE A REPLY

Please enter your comment!
Please enter your name here