Большой объем памяти для целых чисел по сравнению с результатом sys.getsizeof()

Объектам Python-Integer в диапазоне [1,2^30) требуется 28 байт, как это предусмотрено sys.getsizeof() и объяснено, например, в этот SO-пост.

Однако, когда я измеряю объем памяти с помощью следующего скрипта:

#int_list.py:
import sys

N=int(sys.argv[1])
lst=[0]*N            # no overallocation

for i in range(N):
    lst[i]=1000+i    # ints not from integer pool

через

/usr/bin/time -fpeak_used_memory:%M python3 int_list.py <N>

Я получаю следующие пиковые значения памяти (Linux-x64, Python 3.6.2):

   N     Peak memory in Kb        bytes/integer
-------------------------------------------   
   1            9220              
   1e7        404712                40.50 
   2e7        800612                40.52 
   3e7       1196204                40.52
   4e7       1591948                40.52

Получается, что на один целочисленный объект требуется 40.5 байт, то есть на 12.5 байт больше, чем дает sys.getsizeof().

Дополнительные 8 байты легко объяснить - список lst содержит не целочисленные объекты, а ссылки на них - значит, нужен дополнительный указатель, т.е. 8 байты.

Однако как насчет остальных 4.5 байтов, для чего они используются?

Можно исключить следующие причины:

  • размер целочисленных объектов является переменным, но 10^7 меньше, чем 2^30, и, таким образом, все целые числа будут иметь размер 28 байт.
  • В списке lst нет перераспределения, что можно легко проверить с помощью sys.getsizeof(lst), что дает в 8 раз больше элементов, плюс очень небольшие накладные расходы.
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
4
0
221
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Для объекта int требуется всего 28 байт, но Python использует выравнивание по 8 байтам: память выделяется блоками, размер которых кратен 8 байтам. Таким образом, фактическая память, используемая каждым int объектом, составляет 32 байта. См. эту прекрасную статью о Управление памятью Python для более подробной информации.

У меня пока нет объяснения для оставшегося полубайта, но я обновлю его, если найду.

На самом деле очень хорошая догадка! Однако ситуация более тонкая. Смотрите мой ответ (который также объясняет 0,5 байта).

ead 10.04.2019 23:57
Ответ принят как подходящий

Предложение @Nathan на удивление не является решением из-за некоторых тонкостей longint-реализации CPython. С его объяснением объем памяти для

...
lst[i] = (1<<30)+i

все еще должно быть 40.52, потому что sys.sizeof(1<<30) это 32, но измерения показывают, что это 48.56. С другой стороны, для

...
lst[i] = (1<<60)+i

след по-прежнему 48.56, несмотря на то, что sys.sizeof(1<<60) это 36.

Причина: sys.getsizeof() не сообщает нам реальный объем памяти для результата суммирования, то есть a+b который

  • 32 байта для 1000+i
  • 36 байт для (1<<30)+i
  • 40 байт для (1<<60)+i

Это происходит потому, что при добавлении двух целых чисел в x_add результирующее целое имеет сначала одну «цифру», то есть 4 байта, больше, чем максимум a и b:

static PyLongObject *
x_add(PyLongObject *a, PyLongObject *b)
{
    Py_ssize_t size_a = Py_ABS(Py_SIZE(a)), size_b = Py_ABS(Py_SIZE(b));
    PyLongObject *z;
    ...
    /* Ensure a is the larger of the two: */
    ...
    z = _PyLong_New(size_a+1);  
    ...

после сложения результат нормализуется:

 ...
 return long_normalize(z);

};

т.е. возможные ведущие нули отбрасываются, но память не освобождается - 4 байта не стоят, источник функции можно найти здесь.


Теперь мы можем использовать понимание @Nathans, чтобы объяснить, почему след (1<<30)+i равен 48.56, а не 44.xy: используемый py_malloc-аллокатор использует блоки памяти с выравниванием 8 байтов, что означает, что 36 байты будут храниться в блоке размером 40 - то же, что и результат (1<<60)+i (помните о дополнительных 8 байтах для указателей).


Чтобы объяснить оставшиеся 0.5 байты, нам нужно углубиться в детали py_malloc-распределителя. Хорошим обзором является сам исходный код, моя последняя попытка описать его может быть найдена в этом SO-пост.

В двух словах, распределитель управляет памятью в аренах, каждая по 256 МБ. Когда арена выделена, память резервируется, но не фиксируется. Мы видим память как «использованную» только тогда, когда затрагивается так называемая pool. Пул 4Kb большой (POOL_SIZE) и используется только для блоков памяти одинакового размера — в нашем случае 32 байт. Это означает, что разрешение peak_used_memory составляет 4 КБ и не может отвечать за эти 0.5 байты.

Однако этими пулами необходимо управлять, что приводит к дополнительным накладным расходам: py_malloc требуется pool_header на пул:

/* Pool for small blocks. */
struct pool_header {
    union { block *_padding;
            uint count; } ref;          /* number of allocated blocks    */
    block *freeblock;                   /* pool's free list head         */
    struct pool_header *nextpool;       /* next pool of this size class  */
    struct pool_header *prevpool;       /* previous pool       ""        */
    uint arenaindex;                    /* index into arenas of base adr */
    uint szidx;                         /* block size class index        */
    uint nextoffset;                    /* bytes to virgin block         */
    uint maxnextoffset;                 /* largest valid nextoffset      */
};

Размер этой структуры составляет 48 (называется POOL_OVERHEAD) байт на моей машине Linux_64. Этот pool_header является частью пула (довольно умный способ избежать дополнительного выделения через cruntime-memory-allocator) и будет занимать место двух 32-байтовых блоков, то есть пул имеет место для 12632 байтовых целых чисел:

/* Return total number of blocks in pool of size index I, as a uint. */
#define NUMBLOCKS(I) ((uint)(POOL_SIZE - POOL_OVERHEAD) / INDEX2SIZE(I))

Что приводит к:

  • 4Kb/126 = 32.51 байтов для 1000+i плюс дополнительные 8 байтов для указателя.
  • (30<<1)+i требуется 40 байт, значит, в 4Kb есть место для 102 блоков, из которых один (осталось 16 байт при разделении пула на 40-байтовый блок, и их можно использовать для pool_header) используется для pool_header, что приводит к 4Kb/101=40.55 байт (плюс 8 указатель байта).

Мы также можем видеть, что есть некоторые дополнительные накладные расходы, ответственные за ок. 0.01 байт на целое число - недостаточно большой, чтобы меня это волновало.

Другие вопросы по теме