[python] 튜플이 목록보다 메모리 공간을 덜 차지하는 이유는 무엇입니까?

A tuple는 Python에서 메모리 공간을 덜 차지합니다.

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

반면 listS 더 많은 메모리 공간을 취

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

Python 메모리 관리에서 내부적으로 어떤 일이 발생합니까?



답변

CPython과 64 비트를 사용하고 있다고 가정합니다 (CPython 2.7 64 비트에서 동일한 결과를 얻었습니다). 다른 Python 구현이나 32 비트 Python이있는 경우에는 차이가있을 수 있습니다.

구현에 관계없이 lists는 가변 크기이고 tuples는 고정 크기입니다.

따라서 tuples는 구조체 내부에 요소를 직접 저장할 수 있고, 반면에 목록에는 간접 레이어가 필요합니다 (요소에 대한 포인터를 저장합니다). 이 간접 계층은 64 비트 시스템에서 포인터이며, 따라서 8 바이트입니다.

하지만 또 다른 일 list이 있습니다. 그들은 과잉 할당합니다. 그렇지 않으면 항상 작업 list.append이 될 것입니다 .-상각 (훨씬 더 빠르게 !!!)하기 위해 과도하게 할당합니다. 그러나 이제는 할당 된 크기와 채워진 크기 추적해야합니다 ( 할당 된 크기 와 채워진 크기는 항상 동일하기 때문에 하나의 크기 만 저장하면됩니다). 즉, 각 목록은 64 비트 시스템에서 다시 8 바이트 인 64 비트 정수인 또 다른 “크기”를 저장해야합니다.O(n)O(1)tuple

따라서 lists는 tuples 보다 16 바이트 이상의 메모리가 필요합니다 . 내가 왜 “적어도”라고 말했습니까? 초과 할당 때문입니다. 초과 할당은 필요한 것보다 더 많은 공간을 할당 함을 의미합니다. 그러나 초과 할당량은 목록을 만드는 “방법”과 추가 / 삭제 기록에 따라 다릅니다.

>>> 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은 작은 정수도 재사용 하기 때문에 실제로는 근사치 입니다. 따라서 메모리에있는 객체의 더 정확한 표현 (읽을 수는 없지만)은 다음과 같습니다.

여기에 이미지 설명 입력

유용한 링크:

참고 __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_typeof 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_HEAD3 개의 8 바이트 필드 ( ob_refcount, ob_typeob_size) 로 확장 되므로 24바이트 기여도입니다.

그래서 지금 res은 :

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

또는:

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

목록 인스턴스에 할당 된 요소가있는 경우. 두 번째 부분은 기여도를 계산합니다. self->allocated, 이름에서 알 수 있듯이 할당 된 요소의 수를 보유합니다.

요소가 없으면 목록의 크기는 다음과 같이 계산됩니다.

>>> [].__sizeof__()
40

즉, 인스턴스 구조체의 크기.


tuple객체는 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);
}

이것은 lists tp_basicsize와 마찬가지로 객체가 0이 아닌 경우 tp_itemsize(가변 길이 인스턴스가 있음을 의미) 튜플의 항목 수 Py_SIZEtp_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_basicsizeof tuples는 실제로 다음과 같이 계산됩니다.

sizeof(PyTupleObject) - sizeof(PyObject *)

추가 8바이트가 제거되는 이유는 tp_basicsize내가 알 수 없었던 것입니다. (가능한 설명은 MSeifert의 의견을 참조하십시오)


그러나 이것은 기본적으로 특정 예의 차이점 입니다 . lists는 또한 할당 된 요소의 수를 유지하여 언제 다시 초과 할당 할지를 결정하는 데 도움이됩니다.

이제 추가 요소가 추가되면 목록은 실제로 O (1) 추가를 달성하기 위해이 초과 할당을 수행합니다. MSeifert의 답변에서 멋지게 다루기 때문에 크기가 더 커집니다.


답변

MSeifert 답변은 광범위하게 다루고 있습니다. 단순하게 유지하려면 다음을 생각할 수 있습니다.

tuple불변입니다. 일단 설정되면 변경할 수 없습니다. 따라서 해당 객체에 할당해야하는 메모리 양을 미리 알고 있습니다.

list변경 가능합니다. 여기에 항목을 추가하거나 제거 할 수 있습니다. 크기를 알아야합니다 (내부 impl.). 필요에 따라 크기가 조정됩니다.

무료 식사는 없습니다. 이러한 기능에는 비용이 따릅니다. 따라서 목록에 대한 메모리 오버 헤드.


답변

튜플의 크기는 접두사로 지정됩니다. 즉, 튜플 초기화시 인터프리터가 포함 된 데이터에 대해 충분한 공간을 할당하고 그게 끝이어서 변경 불가능 (수정할 수 없음)을 제공하는 반면 목록은 변경 가능한 객체이므로 동적을 의미합니다. 메모리 할당, 따라서 목록을 추가하거나 수정할 때마다 공간 할당을 피하기 위해 (변경된 데이터를 포함하고 데이터를 복사 할 수있는 충분한 공간을 할당) 향후 추가, 수정 등을 위해 추가 공간을 할당합니다. 요약하다.


답변