Dlaczego dwie identyczne listy mają inny ślad pamięci?

155

Utworzyłem dwie listy l1i l2, ale każda z inną metodą tworzenia:

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))

Ale wyjście mnie zaskoczyło:

Size of l1 = 144
Size of l2 = 192

Lista utworzona za pomocą funkcji list złożonych ma większy rozmiar w pamięci, ale w przeciwnym razie obie listy są identyczne w Pythonie.

Dlaczego? Czy to jakaś wewnętrzna sprawa CPythona, czy jakieś inne wyjaśnienie?

Andrej Kesely
źródło
2
Prawdopodobnie operator powtórzenia wywoła jakąś funkcję, która dokładnie określa rozmiar tablicy bazowej. Zwróć uwagę, że 144 == sys.getsizeof([]) + 8*10)gdzie 8 to rozmiar wskaźnika.
juanpa.arrivillaga
1
Zauważ, że jeśli zmienisz 10na 11, [None] * 11lista będzie miała rozmiar 152, ale lista nadal będzie miała rozmiar 192. Wcześniej połączone pytanie nie jest dokładnym duplikatem, ale ma znaczenie dla zrozumienia, dlaczego tak się dzieje.
Patrick Haugh,

Odpowiedzi:

162

Kiedy piszesz [None] * 10 , Python wie, że będzie potrzebował listy dokładnie 10 obiektów, więc przydziela dokładnie to.

Kiedy używasz rozumienia list, Python nie wie, ile będzie potrzebować. Tak więc lista stopniowo rośnie w miarę dodawania elementów. Przy każdej ponownej alokacji przydziela więcej miejsca, niż jest natychmiast potrzebne, więc nie musi ponownie przydzielać dla każdego elementu. Wynikowa lista prawdopodobnie będzie nieco większa niż potrzeba.

Możesz zobaczyć to zachowanie, porównując listy utworzone o podobnych rozmiarach:

>>> 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

Możesz zobaczyć, że pierwsza metoda przydziela tylko to, co jest potrzebne, podczas gdy druga rośnie okresowo. W tym przykładzie przydziela wystarczającą ilość na 16 elementów i musiał ponownie przydzielić po osiągnięciu 17.

interjay
źródło
1
Tak, to ma sens. Prawdopodobnie lepiej jest tworzyć listy, *gdy znam rozmiar z przodu.
Andrej Kesely,
27
@AndrejKesely Używaj tylko [x] * nz niezmiennymi xna liście. Wynikowa lista będzie zawierała odniesienia do identycznego obiektu.
schwobaseggl
5
@schwobaseggl Cóż, może to być to, czego chcesz, ale dobrze jest to zrozumieć.
juanpa.arrivillaga
19
@ juanpa.arrivillaga Prawda, może być. Ale zwykle tak nie jest, a szczególnie SO jest pełne plakatów zastanawiających się, dlaczego wszystkie ich dane zmieniły się jednocześnie: D
schwobaseggl.
50

Jak zauważono w tym pytaniu, rozumienie listy jest używane list.appendpod maską, więc wywoła metodę zmiany rozmiaru listy, która z nadmierną alokacją.

Aby zademonstrować to sobie, możesz faktycznie użyć disdezasemblera:

>>> 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
>>>

Zwróć uwagę na LIST_APPENDopcode w dezasemblacji <listcomp>obiektu kodu. Z dokumentów :

LIST_APPEND (i)

Połączenia list.append(TOS[-i], TOS). Używane do implementacji list składanych.

Teraz, jeśli chodzi o operację powtarzania listy, mamy wskazówkę dotyczącą tego, co się dzieje, jeśli weźmiemy pod uwagę:

>>> import sys
>>> sys.getsizeof([])
64
>>> 8*10
80
>>> 64 + 80
144
>>> sys.getsizeof([None]*10)
144

Wydaje się więc, że jest w stanie dokładnie przydzielić rozmiar. Patrząc na kod źródłowy , widzimy dokładnie, co się dzieje:

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);

Mianowicie tutaj: size = Py_SIZE(a) * n;. Reszta funkcji po prostu wypełnia tablicę.

juanpa.arrivillaga
źródło
„Jak zauważono w tym pytaniu, rozumienie listy używa list.append pod maską” Myślę, że dokładniej jest powiedzieć, że używa .extend().
Akumulacja
@Acccumulation, dlaczego tak uważasz?
juanpa.arrivillaga
Ponieważ nie dołącza elementów jeden po drugim. Dołączając elementy do listy, tak naprawdę tworzysz nową listę z nową alokacją pamięci i umieszczasz listę w tej nowej alokacji pamięci. Z drugiej strony, wyrażenia listowe umieszczają większość nowych elementów w pamięci, która została już przydzielona, ​​a gdy zabraknie im przydzielonej pamięci, przydzielają kolejny porcję pamięci, nie tylko dla nowego elementu.
Akumulacja
7
@Acccumulation To jest nieprawidłowe. list.appendjest amortyzowaną operacją o stałym czasie, ponieważ zmiana rozmiaru listy powoduje nadmierną alokację. Dlatego nie każda operacja dołączania skutkuje nowo przydzieloną tablicą. W każdym razie kwestia, że związana pokazuje w kodzie źródłowym, że w rzeczywistości listowych zrobić stosowanie list.append,. Zaraz wrócę do swojego laptopa i pokażę wam zdemontowany kod bajtowy dla zrozumienia listy i odpowiedniego LIST_APPENDkodu
operacji
3

Żaden jest blokiem pamięci, ale nie ma wstępnie określonego rozmiaru. Oprócz tego istnieje dodatkowe odstępy w tablicy między elementami tablicy. Możesz to zobaczyć, uruchamiając:

for ele in l2:
    print(sys.getsizeof(ele))

>>>>16
16
16
16
16
16
16
16
16
16

Co nie sumuje rozmiaru l2, ale raczej jest mniejsze.

print(sys.getsizeof([None]))
72

A to znacznie więcej niż jedna dziesiąta rozmiaru l1 .

Twoje liczby powinny się różnić w zależności zarówno od szczegółów systemu operacyjnego, jak i szczegółów bieżącego wykorzystania pamięci w systemie operacyjnym. Rozmiar [None] nigdy nie może być większy niż dostępna sąsiednia pamięć, w której zmienna ma być przechowywana, a zmienna może wymagać przeniesienia, jeśli zostanie później przydzielona dynamicznie, aby była większa.

StevenJD
źródło
1
Nonew rzeczywistości nie jest przechowywany w podstawowej tablicy, jedyną rzeczą, która jest przechowywana, jest PyObjectwskaźnik (8 bajtów). Wszystkie obiekty Pythona są przydzielane na stercie. Nonejest singletonem, więc posiadanie listy z wieloma wartościami zerowymi po prostu utworzy tablicę wskaźników PyObject do tego samego Noneobiektu na stercie (bez użycia dodatkowej pamięci w procesie na dodatkowy None). Nie jestem pewien, co masz na myśli, mówiąc „Żaden nie ma określonego rozmiaru”, ale to nie brzmi poprawnie. Wreszcie, twoja pętla z getsizeofkażdym elementem nie demonstruje tego, co wydaje ci się, że demonstruje.
juanpa.arrivillaga
Jeśli tak, jak mówisz, rozmiar [Brak] * 10 powinien być taki sam jak rozmiar [Brak]. Ale najwyraźniej tak nie jest - dodano dodatkowe miejsce do przechowywania. W rzeczywistości rozmiar [Brak] powtórzony dziesięć razy (160) jest również mniejszy niż rozmiar [Brak] pomnożony przez dziesięć. Jak zauważyłeś, wyraźnie rozmiar wskaźnika do [None] jest mniejszy niż rozmiar samego [None] (16 bajtów zamiast 72 bajtów). Jednak 160 + 32 to 192. Nie sądzę, aby poprzednia odpowiedź całkowicie rozwiązała problem. Oczywiste jest, że przydzielana jest dodatkowa niewielka ilość pamięci (być może zależna od stanu komputera).
StevenJD
„Jeśli tak, jak mówisz, rozmiar [Brak] * 10 powinien być taki sam jak rozmiar [Brak]”, co ja mówię, co mogłoby to sugerować? Ponownie, wydaje się, że koncentrujesz się na tym, że podstawowy bufor jest nadmiernie przydzielony lub że rozmiar listy obejmuje więcej niż rozmiar bazowego bufora (oczywiście tak jest), ale nie o to chodzi to pytanie. Ponownie, użycie gestsizeofna każdym elez nich l2jest mylące, ponieważ getsizeof(l2) nie uwzględnia rozmiaru elementów wewnątrz kontenera .
juanpa.arrivillaga
Aby udowodnić sobie to ostatnie twierdzenie, zrób l1 = [None]; l2 = [None]*100; l3 = [l2]to print(sys.getsizeof(l1), sys.getsizeof(l2), sys.getsizeof(l3)). dostaniesz wynik takiego: 72 864 72. Oznacza to, odpowiednio 64 + 1*8, 64 + 100*8i 64 + 1*8, ponownie, zakładając system 64bit z 8 bajtów wielkości wskaźnika.
juanpa.arrivillaga
1
Jak już wspomniałem, sys.getsizeof* nie uwzględnia rozmiaru przedmiotów w kontenerze. Z dokumentacji : „ Uwzględniane jest tylko zużycie pamięci bezpośrednio przypisane do obiektu, a nie zużycie pamięci przez obiekty, do których się ono odnosi ... Zobacz rekurencyjny przepis sizeof, aby zapoznać się z przykładem użycia metody getizeof () w celu znalezienia rozmiaru kontenerów i całą ich zawartość. "
juanpa.arrivillaga