Topic: [RISOLTO] Mistero su sys.getsizeof applicato alle liste  (Letto 251 volte)

0 Utenti e 1 Visitatore stanno visualizzando questo topic.

Offline marcus72

  • python unicellularis
  • *
  • Post: 32
  • Punti reputazione: 0
    • Mostra profilo
[RISOLTO] Mistero su sys.getsizeof applicato alle liste
« il: Novembre 24, 2018, 16:59 »
Scusatemi: ma sys.getsizeof(lista) come può dare una dimensione molto minore di sum([sys.getsizeof(x) for x in lista])  (cioè la somma di tutti i suoi elementi)? Per avere quindi lo spazio totale occupato in ram di una lista come si può fare?

Mi spiego meglio con un esempio nell'interprete di Python 3.5.2:

>>> s0 = str(10**49)
>>> sys.getsizeof(s0 + '0')
100

Quindi una stringa di 51 caratteri occupa in memoria 100B. Mi sarei aspettato 51B (visto che le stringhe sono composte solo da cifre numeriche, che sono quindi caratteri Ascii). Ma va benissimo: non è questo il problema.

>>> l = [s0 + str(i) for i in range(10)]
>>> l
['100000000000000000000000000000000000000000000000000', '100000000000000000000000000000000000000000000000001', '100000000000000000000000000000000000000000000000002', '100000000000000000000000000000000000000000000000003', '100000000000000000000000000000000000000000000000004', '100000000000000000000000000000000000000000000000005', '100000000000000000000000000000000000000000000000006', '100000000000000000000000000000000000000000000000007', '100000000000000000000000000000000000000000000000008', '100000000000000000000000000000000000000000000000009']
>>> sys.getsizeof(l)
192

Quindi una lista di 10 stringhe da 51 caratteri Ascii l'una (e tutte diverse, così da non permettere a Python di mantenere in memoria una sola copia), di cui ognuna occupa in memoria 100B, al posto di occupare 1000B (o anche qualcosa in più o qualcosa in meno), occupa 192B?? Come può essere? Praticamente sono 510 caratteri, come minimo dovrebbe occupare 510B (in realtà dovrebbe essere qualcosa di più, visto che c'è bisogno di spazio per organizzare la lista, e magari anche qualche altra cosetta in più per accelerare eventuali nuove aggiunte di elementi...

Altro esempio (sempre con Python 3.5.2):

>>> f = 1.5
>>> sys.getsizeof(f)
24

Quindi un float occupa in memoria 24B.

>>> l2 = [f**i for i in range(1, 101)]
>>> sys.getsizeof(l2[-1])
24

Mi conferma che ogni float appartenente alla lista l2 occupa sempre 24B.

>>> sys.getsizeof(l2)
912

Ed ecco di nuovo la grossa sorpresa: 100 float dovrebbero occupare 2400B, mentre infilati in una lista sembrano occupare appena 912B, due volte e mezzo di meno!! Come può essere?
« Ultima modifica: Novembre 24, 2018, 20:03 da marcus72 »

Offline marcus72

  • python unicellularis
  • *
  • Post: 32
  • Punti reputazione: 0
    • Mostra profilo
Re:Mistero su sys.getsizeof applicato alle liste
« Risposta #1 il: Novembre 24, 2018, 17:31 »
Per il secondo esempio, con i float, un'anima pia mi ha illuminato.
Una lista vuota ha 64B di overhead:

>>> lista = []
>>> sys.getsizeof(lista)
64

Un python float ha 16B di overhead e quindi invece di consumare 8B ne consuma 24B (perché è un oggetto):

>>> x = 9.7         
>>> sys.getsizeof(x)
24

Invece, l'occupazione di memoria di una lista di float è data dalla somma dell'overhead della lista + l'occupazione del float32 (senza overhead):

>>> lista = [6.7, 8.9]
>>> sys.getsizeof(lista)
80

Che è corretto!
64B (overhead lista) + 2*8B (float32 senza overhead) = 80B

Quindi nel caso del mio secondo esempio, lo spazio teorico occupato dovrebbe essere
64B (overhead lista) + 100*8B (float32 senza overhead) = 864B
che poco si discosta dai 912B (i 48B in più sono usati dalla lista per gestire 100 posizioni, e probabilmente ci sono anche un po' di posti extra per accogliere eventuali nuovi elementi in modo più veloce).

Certo, rimane per me ancora il mistero della gestione delle stringhe in una lista...

Offline RicPol

  • python sapiens sapiens
  • ******
  • Post: 2.862
  • Punti reputazione: 9
    • Mostra profilo
Re:Mistero su sys.getsizeof applicato alle liste
« Risposta #2 il: Novembre 24, 2018, 18:35 »
Uhm, non so quanto l'anima pia ti abbia davvero illuminato. Una lista in cpython è implementata essenzialmente come una lista di puntatori agli oggetti che contiene. Quindi non mi meraviglia affatto che sia più "piccola" della somma del suo contenuto. Un elemento della lista può essere un oggetto gigantesco, ma nella lista viene conservato solo il puntatore ad esso. Il fatto che i puntatori siano a loro volta dei numeri può spiegare la coincidenza per cui se la lista contiene numeri, allora sembra che "il conto torni". Ma è una coincidenza, appunto.

Offline marcus72

  • python unicellularis
  • *
  • Post: 32
  • Punti reputazione: 0
    • Mostra profilo
Re:Mistero su sys.getsizeof applicato alle liste
« Risposta #3 il: Novembre 24, 2018, 20:02 »
Uhm, non so quanto l'anima pia ti abbia davvero illuminato. Una lista in cpython è implementata essenzialmente come una lista di puntatori agli oggetti che contiene. Quindi non mi meraviglia affatto che sia più "piccola" della somma del suo contenuto. Un elemento della lista può essere un oggetto gigantesco, ma nella lista viene conservato solo il puntatore ad esso. Il fatto che i puntatori siano a loro volta dei numeri può spiegare la coincidenza per cui se la lista contiene numeri, allora sembra che "il conto torni". Ma è una coincidenza, appunto.

Mi sa che hai ragione, perché una lista di 10 float e un'altra di 10 stringhe da 51 caratteri l'una occupano esattamente entrambe 192B.
Non è che voglio criticare questa cosa: basta saperlo e sommare, allo spazio occupato dalla lista anche quello occupato dai suoi elementi.
Comunque grazie!

Offline RicPol

  • python sapiens sapiens
  • ******
  • Post: 2.862
  • Punti reputazione: 9
    • Mostra profilo
Re:[RISOLTO] Mistero su sys.getsizeof applicato alle liste
« Risposta #4 il: Novembre 24, 2018, 20:09 »
Non ci siamo. Se una lista contiene altre liste, o se contiene oggetti che contengono riferimenti ad altri oggetti... etc, etc, (i casi possibili sono zillioni), non riuscirai comunque a calcolare "quanta memoria occupa la lista". E poi: hai la lista, ma hai anche lo spazio in memoria che il reference counting dedica a quella lista. Conti anche quello, o no? Eccetera eccetera. In generale, in un linguaggio di alto livello come Python, ha poco senso mettersi a fare conti del genere. Poi certo, ci sono scenari in cui potrebbe servirti, per esempio quando stai dando la caccia a un baco.

Offline marcus72

  • python unicellularis
  • *
  • Post: 32
  • Punti reputazione: 0
    • Mostra profilo
Re:[RISOLTO] Mistero su sys.getsizeof applicato alle liste
« Risposta #5 il: Dicembre 16, 2018, 23:54 »
Non ci siamo. Se una lista contiene altre liste, o se contiene oggetti che contengono riferimenti ad altri oggetti... etc, etc, (i casi possibili sono zillioni), non riuscirai comunque a calcolare "quanta memoria occupa la lista". E poi: hai la lista, ma hai anche lo spazio in memoria che il reference counting dedica a quella lista. Conti anche quello, o no? Eccetera eccetera. In generale, in un linguaggio di alto livello come Python, ha poco senso mettersi a fare conti del genere. Poi certo, ci sono scenari in cui potrebbe servirti, per esempio quando stai dando la caccia a un baco.
Se una lista contiene altre liste, o tuple, non è un problema: basta fare una funzione di conteggio ricorsiva. Pensavo fosse ovvio che questo discorso si fa per un motivo pratico, ovvero tenere il conto di quanta memoria RAM si usa, magari per tenere sotto controllo algoritmi che possono mangiare vari GB di RAM. L'unica pecca è dovuta al fatto che Python cerca di economizzare la memoria occupata (per fortuna), per cui, per esempio, i numeri interi "piccoli", così come None, vengono memorizzati una sola volta, e poi varie istanze puntano allo stesso oggetto. Si può svelare questo meccanismo con "is". Se fai

Python 3.5.2 (default, Nov 12 2018, 13:43:14)
>>> a = 1
>>> b = 1
>>> a == b
True
>>> a is b
True
>>> a = 1000
>>> b = 1000
>>> a == b
True
>>> a is b
False

Ma poco male: la cifra che trova l'algoritmo di conteggio può essere di poco superiore alla memoria effettiva calcolata (si potrebbe, tramite "is", calcolare la memoria effettiva, ma il costo computazionale può essere troppo esoso, e per tenere semplicemente sotto controllo la crescita della lista non ne vale la pena).
Se a qualcuno può interessare:

def sizeof(oggetto) -> int:
    """
    Spazio effettivo totale occupato in memoria da un oggetto.
    Può errare per eccesso (due link in memoria potrebbero puntare allo
    stesso oggetto, come per esempio interi "piccoli" e None).
    """
    out = sys.getsizeof(oggetto)
    try:  # dict
        out += (sum( (sizeof(x) for x in oggetto.keys()) ) +
                sum( (sizeof(x) for x in oggetto.values()) ))
    except (TypeError, AttributeError):
        if type(oggetto) is not str:
            try:  # list, tuple, set, frozenset
                out += sum( (sizeof(x) for x in oggetto) )
            except TypeError:
                pass
    return out

Offline RicPol

  • python sapiens sapiens
  • ******
  • Post: 2.862
  • Punti reputazione: 9
    • Mostra profilo
Re:[RISOLTO] Mistero su sys.getsizeof applicato alle liste
« Risposta #6 il: Dicembre 17, 2018, 22:25 »
> Se una lista contiene altre liste, o tuple, non è un problema: basta fare una funzione di conteggio ricorsiva.
Certamente, ma non è quello che dicevo io. Un oggetto in python resta in vita (occupa memoria) finché almeno una variabile lo referenzia. Quello che dicevo io è: ci sono molti modi per cui un oggetto qualsiasi può "contenere" un riferimento a un altro oggetto qualsiasi, non solo il modo in cui una lista contiene altre cose. Per esempio, molto banalmente:

>>> getsizeof(10), getsizeof(10**100)
(28, 72)
>>>
>>> class Nothing: pass
...
>>> class Small:
...     def __init__(self): self.x = 10
...
>>> class Big:
...     def __init__(self): self.x = 10**100
...
>>> n, s, b = Nothing(), Small(), Big()
>>> getsizeof(n), getsizeof(s), getsizeof(b)
(56, 56, 56)

Ed eccoti arrivato a un'altra versione del tuo problema della lista: i tre oggetti "n", "s" e "b" sembrano avere la stessa dimensione, anche se in realtà dentro "contengono" oggetti di peso diverso.
E per questa versione del problema, vedi che la tua soluzione ("non è un problema: basta fare una funzione ricorsiva") già non tiene più.

> Pensavo fosse ovvio che questo discorso si fa per un motivo pratico,
> ovvero tenere il conto di quanta memoria RAM si usa,

Ovvissimo, certamente. L'importante è capire che non funziona, fuori dal tuo caso specifico. Ed è importante capire che se davvero "si fa per un motivo pratico" (ovvero perché magari hai un leak), allora la soluzione è profilare il codice. E fra l'altro è meglio specificare questo anche per chi in futuro dovesse leggere questo thread e magari restare confuso.
Guarda, la funzione ricorsiva che hai scritto la trovi qui https://github.com/the-gigi/deep/blob/master/deeper.py#L80 in una forma leggermente più elegante. Fa parte di questo articolo https://code.tutsplus.com/tutorials/understand-how-much-memory-your-python-objects-use--cms-25609 che ripercorre pari pari i tuoi dubbi di questo thread, la tua soluzione, e il problema fondamentale della tua soluzione: che funziona solo ed esclusivamente per semplici oggetti python (sequenze e mapping, nello specifico). E finisce consigliando la soluzione vera, che sarebbe usare questo https://pypi.org/project/memory-profiler/ (ma ce ne sono anche altri, a partire da questo http://guppy-pe.sourceforge.net/).