Размер списка в памяти

Я просто экспериментировал с размером структур данных python в памяти. Я написал следующий фрагмент:

import sys
lst1=[]
lst1.append(1)
lst2=[1]
print(sys.getsizeof(lst1), sys.getsizeof(lst2))

Я проверил код в следующих конфигурациях:

  • Windows 7 64bit, Python3.1: выход: 52 40, поэтому lst1 имеет 52 байта, а lst2 - 40 байт.
  • Ubuntu 11.4 32bit с Python3.2: вывод 48 32
  • Ubuntu 11.4 32bit Python2.7: 48 36

Может кто-нибудь объяснить мне, почему два размера отличаются, хотя оба являются списками, содержащими 1?

В документации python для функции getizeof я нашел следующее: ...adds an additional garbage collector overhead if the object is managed by the garbage collector. Может ли это быть в моем маленьком примере?

Ответ 1

Здесь более полный интерактивный сеанс, который поможет мне объяснить, что происходит (Python 2.6 на 32-разрядной Windows XP, но это не имеет значения):

>>> import sys
>>> sys.getsizeof([])
36
>>> sys.getsizeof([1])
40
>>> lst = []
>>> lst.append(1)
>>> sys.getsizeof(lst)
52
>>> 

Обратите внимание, что пустой список немного меньше, чем тот, у которого [1]. Однако, когда элемент добавляется, он становится намного больше.

Причиной этого являются детали реализации в Objects/listobject.c, в источнике CPython.

Пустой список

Когда создается пустой список [], пространство для элементов не выделяется - это можно увидеть в PyList_New. 36 байтов - это объем пространства, необходимый для самой структуры данных списка на 32-разрядной машине.

Список с одним элементом

Когда создается список с одним элементом [1], пространство для одного элемента выделяется в дополнение к памяти, требуемой самой структурой списка. Опять же, это можно найти в PyList_New. Учитывая size как аргумент, он вычисляет:

nbytes = size * sizeof(PyObject *);

И затем имеет:

if (size <= 0)
    op->ob_item = NULL;
else {
    op->ob_item = (PyObject **) PyMem_MALLOC(nbytes);
    if (op->ob_item == NULL) {
        Py_DECREF(op);
        return PyErr_NoMemory();
    }
    memset(op->ob_item, 0, nbytes);
}
Py_SIZE(op) = size;
op->allocated = size;

Итак, мы видим, что при size = 1 выделяется пространство для одного указателя. 4 байта (в моем 32-битном поле).

Добавление в пустой список

При вызове append в пустом списке, вот что происходит:

  • PyList_Append вызывает app1
  • app1 запрашивает размер списка (и получает 0 в качестве ответа)
  • app1 затем вызывает list_resize с size+1 (1 в нашем случае)
  • list_resize имеет интересную стратегию распределения, обобщенную в этом комментарии из своего источника.

Вот он:

/* This over-allocates proportional to the list size, making room
* for additional growth.  The over-allocation is mild, but is
* enough to give linear-time amortized behavior over a long
* sequence of appends() in the presence of a poorly-performing
* system realloc().
* The growth pattern is:  0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
*/
new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6);

/* check for integer overflow */
if (new_allocated > PY_SIZE_MAX - newsize) {
    PyErr_NoMemory();
    return -1;
} else {
    new_allocated += newsize;
}

Давайте сделаем некоторую математику

Посмотрим, как достигнуты числа, которые я процитировал в сеансе в начале моей статьи.

Таким образом, 36 байт - это размер, необходимый самой структуре данных списка на 32-битной основе. С одним элементом пространство выделяется для одного указателя, так что 4 дополнительных байта - всего 40 байт. ОК до сих пор.

Когда app1 вызывается в пустом списке, он вызывает list_resize с помощью size=1. Согласно алгоритму перераспределения list_resize, следующий наибольший доступный размер после 1 равен 4, поэтому будет выделено 4 указателя. 4 * 4 = 16 байт и 36 + 16 = 52.

В самом деле, все имеет смысл: -)

Ответ 2

Извините, предыдущий комментарий был немного резким.

что происходит, так это то, что вы смотрите на то, как распределяются списки (и я думаю, может быть, вы просто хотели видеть, насколько велики были вещи - в этом случае используйте sys.getsizeof())

когда что-то добавляется в список, может произойти одна из двух:

  • дополнительный элемент помещается в запасное пространство

  • требуется дополнительное пространство, поэтому создается новый список и содержимое, скопированное в него, и добавленная добавленная вещь.

так как (2) дорого (копирование вещей, даже указателей, занимает время, пропорциональное количеству вещей, которые нужно скопировать, поэтому растет по мере того, как списки становятся большими), мы хотим сделать это нечасто. поэтому вместо того, чтобы просто добавить немного больше места, мы добавим целый кусок. обычно размер добавляемого количества аналогичен тому, что уже используется. Таким образом, математика выясняет, что средняя стоимость выделения памяти, распределенная по многим видам использования, пропорциональна только размеру списка.

поэтому то, что вы видите, связано с этим поведением. я не знаю точных деталей, но я не удивлюсь, если [] или [1] (или оба) являются особыми случаями, в которых выделяется только достаточно памяти (чтобы сохранить память в этих общих случаях), а затем добавление делает "захватить новый кусок", описанный выше, который добавляет больше.

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

ps опрятной альтернативой этому является создание списков в виде пар (value, pointer), где каждый указатель указывает на следующий кортеж. таким образом вы можете увеличивать списки постепенно, хотя общая используемая память выше. это связанный список (то, что использует python, больше похоже на вектор или динамический массив).

[обновление] см. Eli отличный ответ. s/he объясняет, что как [], так и [1] выделяются точно, но добавление к [] выделяет дополнительный кусок. комментарий в коде - это то, что я говорю выше (это называется "избыточным распределением", и количество пропорционально тому, что у нас есть, чтобы средняя ( "амортизированная" ) стоимость была пропорциональна размеру.)

Ответ 3

Здесь представлена ​​быстрая демонстрация шаблона роста списка. Изменение третьего аргумента в range() изменит вывод, чтобы он не выглядел как комментарии в listobject.c, но результат, когда просто добавление одного элемента кажется совершенно точным.

allocated = 0
for newsize in range(0,100,1):
    if (allocated < newsize):
        new_allocated = (newsize >> 3) + (3 if newsize < 9 else 6)
        allocated = newsize + new_allocated;
    print newsize, allocated