Объектам 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
раз больше элементов, плюс очень небольшие накладные расходы.Для объекта int
требуется всего 28 байт, но Python использует выравнивание по 8 байтам: память выделяется блоками, размер которых кратен 8 байтам. Таким образом, фактическая память, используемая каждым int
объектом, составляет 32 байта. См. эту прекрасную статью о Управление памятью Python для более подробной информации.
У меня пока нет объяснения для оставшегося полубайта, но я обновлю его, если найду.
Предложение @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
который
1000+i
(1<<30)+i
(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
-байтовых блоков, то есть пул имеет место для 126
32
байтовых целых чисел:
/* 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
байт на целое число - недостаточно большой, чтобы меня это волновало.
На самом деле очень хорошая догадка! Однако ситуация более тонкая. Смотрите мой ответ (который также объясняет 0,5 байта).