두 개의 동일한 목록에 다른 메모리 공간이있는 이유는 무엇입니까?


155

나는이 목록을 작성 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

리스트 이해력으로 생성 된리스트는 메모리에서 더 큰 크기이지만, 그렇지 않으면 파이썬에서 두리스트는 동일합니다.

왜 그런 겁니까? 이것이 CPython 내부적 인 것입니까, 아니면 다른 설명입니까?


2
아마도 반복 연산자는 기본 배열의 크기를 정확하게 지정하는 함수를 호출 할 것입니다. 참고, 그 144 == sys.getsizeof([]) + 8*10)8 포인터의 크기입니다.
juanpa.arrivillaga

1
참고 변경하는 경우 있음 1011[None] * 11목록 크기를 가지고 152있지만, 지능형리스트는 여전히 크기가 192. 이전에 연결된 질문은 정확히 중복되지 않지만 왜 이런 일이 발생하는지 이해하는 데 관련이 있습니다.
Patrick Haugh

답변:


162

을 쓸 때 [None] * 10Python은 정확히 10 개의 객체 목록이 필요하다는 것을 알고 있으므로 정확히 할당합니다.

리스트 이해를 사용할 때, 파이썬은 그것이 얼마나 필요한지 알지 못합니다. 따라서 요소가 추가됨에 따라 목록이 점차 커집니다. 각 재 할당마다 즉시 필요한 것보다 많은 공간을 할당하므로 각 요소에 대해 재 할당 할 필요가 없습니다. 결과 목록은 필요한 것보다 다소 클 수 있습니다.

비슷한 크기로 작성된 목록을 비교할 때이 동작을 볼 수 있습니다.

>>> 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 일에 다시 할당해야했습니다.


1
그렇습니다. *앞에 크기를 알면 목록을 만드는 것이 좋습니다 .
Andrej Kesely

27
@AndrejKesely 목록에서 [x] * n불변 x으로 만 사용 하십시오. 결과 목록은 동일한 객체에 대한 참조를 보유합니다.
schwobaseggl

5
@ schwobaseggl 잘, 그것은 당신이 원하는 것 일지도 모르지만 그것을 이해하는 것이 좋습니다.
juanpa.arrivillaga

19
@ juanpa.arrivillaga 사실 일 수도 있습니다. 그러나 일반적으로 그렇지 않으며 특히 SO는 왜 모든 데이터가 동시에 변경되었는지 궁금해하는 포스터로 가득합니다. :
schwobaseggl

50

이 질문 에서 언급했듯이 목록 이해력은 list.append후드에서 사용하므로 목록 크기 조정 메소드를 호출하여 전체적으로 할당합니다.

이것을 직접 보여주기 위해 실제로 disdissasembler를 사용할 수 있습니다 :

>>> 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의 디스 어셈블리 에서 opcode를 확인하십시오 <listcomp>. 로부터 문서 :

LIST_APPEND (i)

전화 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;. 나머지 함수는 단순히 배열을 채 웁니다.


"이 질문에서 언급했듯이 list-comprehension은 후드 아래에서 list.append를 사용합니다 .extend()." 라고 말하는 것이 더 정확하다고 생각합니다 .
Accumulation

@Acccumulation 왜 그렇게 믿습니까?
juanpa.arrivillaga

요소를 하나씩 추가하지 않기 때문입니다. 요소를 목록에 추가하면 실제로 새 메모리 할당으로 새 목록을 만들고 새 메모리 할당에 목록을 넣는 것입니다. 반면에,리스트 이해는 대부분의 새로운 요소를 이미 할당 된 메모리에 넣습니다. 그리고 할당 된 메모리가 부족할 때 새로운 요소에 충분하지 않은 다른 메모리 척도를 할당합니다.
Accumulation

7
@Acccumulation 잘못되었습니다. list.append목록의 크기를 조정할 때 전체적으로 할당되므로 상각 된 상수 시간 작업입니다. 따라서 모든 추가 작업이 새로 할당 된 배열이되는 것은 아닙니다. 어쨌든 내가 링크 한 질문은 실제로 소스 코드에서 목록 이해 사용 하는 소스 코드를 보여줍니다 list.append. 잠시 후 랩톱으로 돌아와서 목록 이해 및 해당 LIST_APPENDopcode에 대한 분해 된 바이트 코드를 보여줄 수 있습니다
juanpa.arrivillaga

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

그리고 이것은의 크기의 10 분의 1보다 훨씬 큽니다 l1.

숫자는 운영 체제의 세부 사항과 운영 체제의 현재 메모리 사용량의 세부 사항에 따라 달라집니다. [없음]의 크기는 변수가 저장되도록 설정된 사용 가능한 인접 메모리보다 클 수 없으며 나중에 동적으로 더 크게 할당 된 경우 변수를 이동해야 할 수도 있습니다.


1
None기본 배열에 실제로 저장되어 있지는 않지만 저장되는 유일한 것은 PyObject포인터 (8 바이트)입니다. 모든 Python 객체는 힙에 할당됩니다. None단일 항목이므로 많은 목록이 None없는 것은 힙 의 동일한 객체에 대한 PyObject 포인터 배열을 만드는 것입니다 (추가 당 프로세스에서 추가 메모리를 사용하지 않음 None). "None에 미리 지정된 크기가 없습니다"라는 의미가 무엇인지 잘 모르겠지만 정확한 소리는 아닙니다. 마지막으로, getsizeof각 요소를 가진 루프 는 당신이 보여주고 있다고 생각하는 것을 보여주지 않습니다.
juanpa.arrivillaga

말한대로 [없음] * 10의 크기는 [없음]의 크기와 같아야합니다. 그러나 분명히 그렇지 않습니다. 일부 추가 스토리지가 추가되었습니다. 실제로, 10 회 반복 된 (None)의 크기 (160)는 또한 [None]의 크기에 10을 곱한 것보다 작다. 지적했듯이 [None]에 대한 포인터의 크기는 [None] 자체의 크기보다 작습니다 (72 바이트가 아닌 16 바이트). 그러나 160 + 32는 192입니다. 위의 답변으로도 문제가 완전히 해결되지는 않습니다. 여분의 소량의 메모리 (아마도 머신 상태에 따라 다름)가 할당되어 있음이 분명합니다.
StevenJD

"만약 당신이 말하는 것처럼, [없음] * 10의 크기는 [없음]의 크기와 같아야합니다." 다시 말하지만, 기본 버퍼가 과도하게 할당되었거나 목록의 크기에 기본 버퍼의 크기 (물론)가 포함되어 있다는 사실에 집중하고있는 것 같습니다. 이 질문. 다시 말하지만, 컨테이너 내부의 요소 크기를 고려하지 않기 때문에 gestsizeof각각 ele에 대한 사용 l2이 잘못되었습니다 . getsizeof(l2)
juanpa.arrivillaga

마지막 주장을 스스로 증명하려면 l1 = [None]; l2 = [None]*100; l3 = [l2]다음을 수행하십시오 print(sys.getsizeof(l1), sys.getsizeof(l2), sys.getsizeof(l3)). 다음과 같은 결과가 나타납니다 72 864 72. 즉, 각각 64 + 1*8, 64 + 100*8그리고 64 + 1*8다시 8 바이트 포인터 크기가 64 비트 시스템을 가정.
juanpa.arrivillaga

1
앞서 언급했듯이 sys.getsizeof* 컨테이너의 항목 크기를 설명하지 않습니다. 로부터 문서 : "개체에 직접 기인 만 메모리 소비가 객체가 아닌 메모리 소비, 회계 그것은하려면 ... 참조 의미 재귀를 sizeof 재귀 적으로 컨테이너의 크기를 발견하고 () getsizeof 사용하는 예를 들어 요리법 "모든 내용."
juanpa.arrivillaga
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.