Почему кортежи занимают меньше места в памяти, чем списки?



A tuple занимает меньше места в памяти в Python:



>>> a = (1,2,3)
>>> a.__sizeof__()
48


, тогда как list s занимает больше места в памяти:



>>> b = [1,2,3]
>>> b.__sizeof__()
64


что происходит внутри системы управления памятью Python?

1299   4  

4 ответов:

я предполагаю, что вы используете CPython и с 64 битами (я получил те же результаты на моем CPython 2.7 64-бит). Там могут быть различия в других реализациях Python или если у вас 32-битный питон.

независимо от реализации, list s имеют переменный размер, а tuple s фиксированный размер.

так tupleможет хранить элементы непосредственно внутри структуры, списки на слой (он хранит указатель на элементы). Этот слой косвенность-это указатель, на 64-битных системах это 64bit, следовательно, 8bytes.

но есть еще одна вещь, что lists do: они чрезмерно выделяют. В противном случае list.append будет O(n) операция всегда - чтобы он амортизировался O(1) (гораздо быстрее!!!) его выделяет. Но теперь он должен следить за выделено размер и , заполненной размер (tuple s нужно хранить только один размер, потому что выделенный и заполненный размер всегда идентичный.) Это означает, что каждый список должен хранить другой "размер", который на 64-битных системах является 64-битным целым числом, снова 8 байт.

так list s нужно по крайней мере на 16 байт больше памяти, чем tuples. Почему я сказал "по крайней мере"? Из-за перерасхода средств. Чрезмерное распределение означает, что он выделяет больше места, чем необходимо. Однако сумма перерасчета зависит от того, " как " вы создаете список и историю добавления/удаления:

>>> l = [1,2,3]
>>> l.__sizeof__()
64
>>> l.append(4)  # triggers re-allocation (with over-allocation), because the original list is full
>>> l.__sizeof__()
96

>>> l = []
>>> l.__sizeof__()
40
>>> l.append(1)  # re-allocation with over-allocation
>>> l.__sizeof__()
72
>>> l.append(2)  # no re-alloc
>>> l.append(3)  # no re-alloc
>>> l.__sizeof__()
72
>>> l.append(4)  # still has room, so no over-allocation needed (yet)
>>> l.__sizeof__()
72

изображения

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

вот как он (схематично) хранится в памяти в вашем примере. Я выделил различия с красными (свободными) циклами:

enter image description here

это на самом деле просто приближение, потому что int объекты также являются объектами Python, а CPython даже повторно использует небольшие целые числа, поэтому, вероятно, более точное представление (хотя и не такое читаемое) объекты в памяти будут:

enter image description here

Полезные ссылки:

отметим, что __sizeof__ на самом деле не возвращает "правильный" размер! Он только возвращает размер сохраненных значений. Однако, когда вы используете sys.getsizeof результат разный:

>>> import sys
>>> l = [1,2,3]
>>> t = (1, 2, 3)
>>> sys.getsizeof(l)
88
>>> sys.getsizeof(t)
72

есть 24 "лишних" байт. Это реальные, это накладные расходы сборщика мусора, которые не учитываются в __sizeof__ метод. Это потому, что вы обычно не должны использовать магические методы напрямую-используйте функции, которые знают, как с ними обращаться, в этом случае: sys.getsizeof (который на самом деле добавляет GC накладные расходы к значению, возвращенному из __sizeof__).

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

я собираюсь использовать 64-битные значения здесь, как и вы.


размер lists вычисляется из следующей функцией list_sizeof:

static PyObject *
list_sizeof(PyListObject *self)
{
    Py_ssize_t res;

    res = _PyObject_SIZE(Py_TYPE(self)) + self->allocated * sizeof(void*);
    return PyInt_FromSsize_t(res);
}

здесь Py_TYPE(self) это макрос, который захватывает ob_type на self (возврат PyList_Type), а _PyObject_SIZE это еще один макрос, который захватывает tp_basicsize из этого типа. tp_basicsize рассчитывается как sizeof(PyListObject) здесь PyListObject - это экземпляр структуры.

The PyListObject структура состоит из трех полей:

PyObject_VAR_HEAD     # 24 bytes 
PyObject **ob_item;   #  8 bytes
Py_ssize_t allocated; #  8 bytes

у них есть комментарии (которые я обрезал), объясняющие, что это такое, перейдите по ссылке выше, чтобы прочитать их. PyObject_VAR_HEAD расширяется на три 8 байт поля (ob_refcount,ob_type и ob_size) так 24 вклад байт.

сейчас res - это:

sizeof(PyListObject) + self->allocated * sizeof(void*)

или:

40 + self->allocated * sizeof(void*)

если экземпляр списка содержит выделенные элементы. вторая часть вычисляет их вклад. self->allocated, как это следует из названия, содержит количество выделенных элементов.

без каких-либо элементов, размер списков рассчитывается следующим образом:

>>> [].__sizeof__()
40

т. е. размер экземпляра структура.


tuple объекты не определяют

ответ MSeifert охватывает его широко; чтобы он был простым, вы можете подумать:

tuple незыблем. Как только он установлен, вы не можете изменить его. Таким образом, вы заранее знаете, сколько памяти вам нужно выделить для этого объекта.

list изменчиво. Вы можете добавлять или удалять элементы из него. Он должен знать его размер (для внутреннего impl.). Он изменяется по мере необходимости.

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

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

Comments

    Ничего не найдено.