Почему два идентичных списка имеют разный объем памяти?
Я создал два списка l1
и l2
, но каждый из них имеет другой метод создания:
import sys
l1 = [None] * 10
l2 = [None for _ in range(10)]
print('Size of l1 =', sys.getsizeof(l1))
print('Size of l2 =', sys.getsizeof(l2))
Но результат удивил меня:
Size of l1 = 144
Size of l2 = 192
Список, созданный с учетом списка, имеет больший размер в памяти, но два списка идентичны в Python.
Это почему? Это какая-то внутренняя вещь CPython или какое-то другое объяснение?
Ответы
Ответ 1
Когда вы пишете [None] * 10
, Python знает, что для этого потребуется список ровно 10 объектов, поэтому он выделяет именно это.
Когда вы используете понимание списка, Python не знает, сколько ему потребуется. Таким образом, он постепенно расширяет список по мере добавления элементов. Для каждого перераспределения он выделяет больше места, чем требуется немедленно, так что ему не нужно перераспределять для каждого элемента. Полученный список, вероятно, будет несколько больше, чем необходимо.
Такое поведение можно увидеть при сравнении списков, созданных с одинаковыми размерами:
>>> sys.getsizeof([None]*15)
184
>>> sys.getsizeof([None]*16)
192
>>> sys.getsizeof([None for _ in range(15)])
192
>>> sys.getsizeof([None for _ in range(16)])
192
>>> sys.getsizeof([None for _ in range(17)])
264
Вы можете видеть, что первый метод выделяет только то, что необходимо, а второе - периодически. В этом примере он выделяет достаточно для 16 элементов и должен был перераспределяться при достижении 17-го.
Ответ 2
Как отмечалось в этом вопросе, в представлении списка используется list.append
под капотом, поэтому он вызовет метод list-resize, который будет зависеть.
Чтобы продемонстрировать это себе, вы действительно можете использовать dis
dissasembler:
>>> code = compile('[x for x in iterable]', '', 'eval')
>>> import dis
>>> dis.dis(code)
1 0 LOAD_CONST 0 (<code object <listcomp> at 0x10560b810, file "", line 1>)
2 LOAD_CONST 1 ('<listcomp>')
4 MAKE_FUNCTION 0
6 LOAD_NAME 0 (iterable)
8 GET_ITER
10 CALL_FUNCTION 1
12 RETURN_VALUE
Disassembly of <code object <listcomp> at 0x10560b810, file "", line 1>:
1 0 BUILD_LIST 0
2 LOAD_FAST 0 (.0)
>> 4 FOR_ITER 8 (to 14)
6 STORE_FAST 1 (x)
8 LOAD_FAST 1 (x)
10 LIST_APPEND 2
12 JUMP_ABSOLUTE 4
>> 14 RETURN_VALUE
>>>
Обратите внимание на код операции LIST_APPEND
при разборке объекта кода <listcomp>
. Из документов:
LIST_APPEND (я)
Вызовы list.append(TOS[-i], TOS)
. Используется для реализации списков.
Теперь, для операции повторения списка, мы имеем намек на то, что происходит, если мы рассмотрим:
>>> import sys
>>> sys.getsizeof([])
64
>>> 8*10
80
>>> 64 + 80
144
>>> sys.getsizeof([None]*10)
144
Таким образом, он, похоже, способен точно выделить размер. Рассматривая исходный код, мы видим, что это именно то, что происходит:
static PyObject *
list_repeat(PyListObject *a, Py_ssize_t n)
{
Py_ssize_t i, j;
Py_ssize_t size;
PyListObject *np;
PyObject **p, **items;
PyObject *elem;
if (n < 0)
n = 0;
if (n > 0 && Py_SIZE(a) > PY_SSIZE_T_MAX / n)
return PyErr_NoMemory();
size = Py_SIZE(a) * n;
if (size == 0)
return PyList_New(0);
np = (PyListObject *) PyList_New(size);
А именно, здесь: size = Py_SIZE(a) * n;
, Остальные функции просто заполняют массив.
Ответ 3
None - это блок памяти, но это не заданный размер. В дополнение к этому в массиве между элементами массива имеется некоторое дополнительное расстояние. Вы можете сами убедиться в этом:
for ele in l2:
print(sys.getsizeof(ele))
>>>>16
16
16
16
16
16
16
16
16
16
Который не суммирует размер l2, а меньше.
print(sys.getsizeof([None]))
72
И это намного больше одной десятой от размера l1
.
Ваши номера должны различаться в зависимости от деталей вашей операционной системы и деталей использования текущей памяти в вашей операционной системе. Размер [Нет] никогда не может быть больше, чем доступная смежная память, где переменная должна быть сохранена, и переменная, возможно, придется перемещать, если впоследствии динамически распределено ее больше.