A tuple
займає менше пам’яті в Python:
>>> a = (1,2,3)
>>> a.__sizeof__()
48
тоді як list
s займає більше місця в пам'яті:
>>> b = [1,2,3]
>>> b.__sizeof__()
64
Що відбувається всередині управління пам'яттю Python?
A tuple
займає менше пам’яті в Python:
>>> a = (1,2,3)
>>> a.__sizeof__()
48
тоді як list
s займає більше місця в пам'яті:
>>> b = [1,2,3]
>>> b.__sizeof__()
64
Що відбувається всередині управління пам'яттю Python?
Відповіді:
Я припускаю, що ви використовуєте CPython та 64-бітові (я отримав однакові результати на своєму 64-бітному CPython 2.7). Можуть бути відмінності в інших реалізаціях Python або якщо у вас 32-бітний Python.
Незалежно від реалізації, list
s мають змінний розмір, а tuple
s мають фіксований розмір.
Так tuple
s може зберігати елементи безпосередньо всередині структури, списки з іншого боку потребують шару непрямості (він зберігає вказівник на елементи). Цей шар непрямості є вказівником для 64-бітових систем, які мають 64 біт, отже, 8 байт.
Але є ще одна річ, яка list
робиться: вони перерозподіляють. В іншому випадку list.append
було б в O(n)
операції завжди - щоб зробити його амортизуєтьсяO(1)
(набагато швидше !!!) це по-Розподіляє. Але тепер він повинен відслідковувати виділений розмір і заповнений розмір ( tuple
s потрібно зберігати лише один розмір, оскільки розміщений і заповнений розмір завжди однаковий). Це означає, що кожен список повинен зберігати ще один "розмір", який у 64-бітових системах - це 64-бітове ціле число, знову ж 8 байт.
Так list
s потрібно як мінімум на 16 байт більше пам'яті, ніж tuple
s. Чому я сказав «принаймні»? Через перевитрату. Перевищення коштів означає, що воно виділяє більше місця, ніж потрібно. Однак сума перевитрати залежить від того, як ви створюєте список та історію додавання / видалення:
>>> 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
Я вирішив створити кілька зображень, які супроводжують пояснення вище. Можливо, вони корисні
Ось як це (схематично) зберігається в пам'яті у вашому прикладі. Я виділив відмінності з червоними (вільними) циклами:
Це насправді лише наближення, оскільки int
об’єкти є також об’єктами Python, а CPython навіть повторно використовує невеликі цілі числа, тому, ймовірно, більш точне подання (хоча і не як читабельне) об'єктів у пам'яті було б:
Корисні посилання:
tuple
структура в сховищі CPython для Python 2.7list
структура в сховищі CPython для Python 2.7int
структура в сховищі CPython для Python 2.7Зауважте, що __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__
).
list
виділення пам'яті stackoverflow.com/questions/40018398 / ...
list()
або розумінні списку.
Я заглиблююсь у кодову базу CPython, щоб ми могли побачити, як насправді розраховуються розміри. Чи не в вашому конкретному прикладі , не більше-розподілу були виконані, тому я не буду торкатися що .
Тут я буду використовувати 64-бітні значення, як і ви.
Розмір для list
s обчислюється з наступної функції 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
структура екземпляра.
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
об'єкти не визначають a tuple_sizeof
функцію. Натомість вони використовують object_sizeof
для обчислення їх розміру:
static PyObject *
object_sizeof(PyObject *self, PyObject *args)
{
Py_ssize_t res, isize;
res = 0;
isize = self->ob_type->tp_itemsize;
if (isize > 0)
res = Py_SIZE(self) * isize;
res += self->ob_type->tp_basicsize;
return PyInt_FromSsize_t(res);
}
Це, як і для list
s, захоплює tp_basicsize
і, якщо об'єкт не має нуляtp_itemsize
(тобто має екземпляри змінної довжини), він помножує кількість елементів у кортежі (через який він отримує Py_SIZE
) зtp_itemsize
.
tp_basicsize
знову використовує sizeof(PyTupleObject)
де PyTupleObject
структура містить :
PyObject_VAR_HEAD # 24 bytes
PyObject *ob_item[1]; # 8 bytes
Отже, без будь-яких елементів (тобто Py_SIZE
повертається0
) розмір порожніх кортежів дорівнює sizeof(PyTupleObject)
:
>>> ().__sizeof__()
24
так? Ну ось ось дивацтво, якому я не знайшов поясненняtp_basicsize
з tuple
х фактично розраховується наступним чином :
sizeof(PyTupleObject) - sizeof(PyObject *)
чому додатковий 8
видаляється байт - tp_basicsize
це те, чого я не зміг з’ясувати. (Дивіться коментар MSeifert для можливого пояснення)
Але, це в основному різниця у вашому конкретному прикладі .list
s також тримати навколо декількох виділених елементів, що допомагає визначити, коли знову перерозподілити.
Тепер, коли додаються додаткові елементи, списки справді виконують це перерозподіл, щоб отримати додатки O (1). Це призводить до більших розмірів, оскільки MSeifert чудово висвітлює свою відповідь.
ob_item[1]
здебільшого це заповнювач (тому є сенс, що його відняти від основної величини). tuple
Виділяється використанням PyObject_NewVar
. Я не з'ясував деталей, тому це лише здогадка про здогадку ...
Відповідь MSeifert широко висвітлює її; щоб це було просто, ви можете придумати:
tuple
незмінна. Після встановлення його ви не можете змінити. Тож ви заздалегідь знаєте, скільки пам’яті потрібно виділити для цього об’єкта.
list
є змінним. Ви можете додавати або видаляти елементи з нього. Він повинен знати його розмір (для внутрішніх імп.). Він змінює розмір за потребою.
Безкоштовного харчування немає - ці можливості поставляються із вартістю. Звідси накладні витрати на пам'ять для списків.
Розмір кортежу є префіксом, тобто при ініціалізації кортежу інтерпретатор виділяє достатньо місця для вміщених даних, і це є кінцем, даючи це незмінне (неможливо змінити), тоді як список є об'єктом, що змінюється, що означає динаміку Виділення пам'яті, щоб уникнути виділення місця кожного разу, коли ви додаєте або змінюєте список (виділіть достатньо місця для вмісту змінених даних та копіювання даних до нього), він виділяє додатковий простір для майбутнього додавання, модифікацій, ... що дуже багато резюмує це