Förstå hur mycket minne dina pythonobjekt använder

Python är ett fantastiskt programmeringsspråk. Det är också känt för att vara ganska långsamt, främst beroende på sin enorma flexibilitet och dynamiska egenskaper. För många applikationer och domäner är det inte ett problem på grund av deras krav och olika optimeringstekniker. Det är mindre känt att Python-objektgrafer (kapslade ordböcker av listor och tuplar och primitiva typer) tar en betydande mängd minne. Detta kan vara en mycket allvarligare begränsningsfaktor på grund av dess effekter på caching, virtuellt minne, flerhyresavtal med andra program och i allmänhet utmanande det tillgängliga minnet, vilket är en knapp och dyr resurs.

Det visar sig att det inte är trivialt att räkna ut hur mycket minne faktiskt konsumeras. I den här artikeln går jag igenom invecklingarna av Python-objektets minneshantering och visar hur man mäter det konsumerade minnet exakt.

I denna artikel fokuserar jag enbart på CPython-den primära implementeringen av Pythons programmeringsspråk. Experimenten och slutsatserna här gäller inte för andra Python-implementeringar som IronPython, Jython och PyPy.

Jag sprang också siffrorna på 64-bitars Python 2.7. I Python 3 är siffrorna ibland lite annorlunda (speciellt för strängar som alltid är Unicode), men begreppen är desamma.

Hands-On Exploration av Python Memory Usage

Låt oss först utforska en liten bit och få en konkret känsla av den faktiska minnesanvändningen av Python-objekt.

Den sys.getsizeof () inbyggda funktionen

Standardbibliotekets sys-modul ger getsizeof () -funktionen. Den funktionen accepterar ett objekt (och valfri standard), kallar objektets storlek av() -metoden och returnerar resultatet, så att du kan göra dina föremål inspekterbara också.

Mätning av Pythonobjektets minne

Låt oss börja med några numeriska typer:

"python import sys

sys.getsizeof (5) 24 "

Intressant. Ett heltal tar 24 byte.

python sys.getsizeof (5.3) 24

Hmm ... en flottör tar 24 byte också.

python från decimalimport Decimal sys.getsizeof (Decimal (5.3)) 80

Wow. 80 byte! Det här får dig verkligen att tänka på om du vill representera ett stort antal reella tal som flottor eller decimaler.

Låt oss gå vidare till strängar och samlingar:

"python sys.getsizeof (") 37 sys.getsizeof ('1') 38 sys.getsizeof ('1234') 41

sys.getsizeof (u ") 50 sys.getsizeof (u'1 ') 52 sys.getsizeof (u'1234') 58"

OK. En tom sträng tar 37 byte, och varje ytterligare tecken lägger till en annan byte. Det säger mycket om avvägningarna att hålla flera korta strängar där du betalar de 37 byte-kostnaderna för varje vs. en enda lång sträng där du bara betalar kostnaden en gång en gång.

Unicode-strängar beter sig på samma sätt, förutom att överhead är 50 byte och varje ytterligare tecken lägger till 2 byte. Det är något att överväga om du använder bibliotek som returnerar Unicode-strängar, men din text kan representeras som enkla strängar.

Förresten, i Python 3, är strängar alltid Unicode och overhead är 49 bytes (de sparade en byte någonstans). Bytesobjektet har ett överlag på endast 33 byte. Om du har ett program som bearbetar mycket korta strängar i minnet och du bryr dig om prestanda, överväga Python 3.

python sys.getsizeof ([]) 72 sys.getsizeof ([1]) 88 sys.getsizeof ([1, 2, 3, 4]) 104 sys.getsizeof (['long longlong string'])

Vad pågår? En tom lista tar 72 byte, men varje extra int lägger till bara 8 byte, där storleken på ett int är 24 byte. En lista som innehåller en lång sträng tar bara 80 byte.

Svaret är enkelt. Listan innehåller inte int-objekten själva. Det innehåller bara en 8-byte (på 64-bitars versioner av CPython) pekaren till det aktuella int-objektet. Vad det betyder är att funktionen getsizeof () inte returnerar det faktiska minnet på listan och alla objekt som den innehåller, men bara minnet av listan och pekarna till dess objekt. I nästa avsnitt presenterar jag funktionen deep_getsizeof () som behandlar problemet.

python sys.getsizeof (()) 56 sys.getsizeof ((1)) 64 sys.getsizeof ((1, 2, 3, 4)) 88 sys.getsizeof (('en lång långsträckt sträng')) 64

Historien är liknande för tuples. Överhuvudet av en tom tuple är 56 byte vs. 72 av en lista. Återigen är denna 16 bytes-skillnad per sekvens låghängande frukt om du har en datastruktur med många små, oföränderliga sekvenser.

"python sys.getsizeof (set ()) 232 sys.getsizeof (set ([1)) 232 sys.getsizeof (set ([1, 2, 3, 4])) 232

sys.getsizeof () 280 sys.getsizeof (dict (a = 1)) 280 sys.getsizeof (dict (a = 1, b = 2, c = 3)) 280 "

Satser och ordböcker växer uppenbarligen inte alls när du lägger till objekt, men notera den enorma kostnaden.

Slutsatsen är att Python-objekt har en enorm fast kostnad. Om din datastruktur består av ett stort antal insamlingsobjekt som strängar, listor och ordböcker som innehåller ett litet antal objekt vardera betalar du en tung avgift.

Funktionen deep_getsizeof ()

Nu när jag har räddat dig halvt till döds och visat att sys.getsizeof () bara kan berätta hur mycket minne ett primitivt objekt tar, låt oss ta en titt på en mer adekvat lösning. Funktionen deep_getsizeof () ökar rekursivt och beräknar den faktiska minnesanvändningen av en Python-objektdiagram.

"Python från samlingar Import Kartläggning, Container från sys import getsizeof

def deep_getsizeof (o, ids): "" "Hitta minnesfotavtrycket för ett Python-objekt

Det här är en rekursiv funktion som borrar ner en Python-objektgraf som en ordbok med inbyggda ordböcker med listor över listor och tummar och uppsättningar. Funktionen sys.getsizeof gör endast en tunn storlek. Det räknar varje objekt i en behållare som en pekare, oavsett hur stor det är. : param o: objektet: param ids:: return: "" "d = deep_getsizeof om id (o) i ids: returnera 0 r = getsizeof (o) ids.add (id (o)) om instans ) eller isinstans (0, unicode): returnera r om instans (o, Mapping): returnera r + summa (d (k, ids) + d (v, ids) för k, v i o.iteritems ()) om instans (o, Container): returnera r + summa (d (x, ids) för x i o) returnera r "

Det finns flera intressanta aspekter på denna funktion. Det tar hänsyn till objekt som refereras flera gånger och räknar dem enbart en gång genom att hålla reda på objektets ID. Det andra intressanta inslaget i implementeringen är att det tar full nytta av samlingsmodulens abstrakta basklasser. Det gör att funktionen är mycket kortfattad att hantera en samling som implementerar antingen mappning eller containerbasklassen istället för att hantera direkt med myriad samlingstyper som: sträng, Unicode, byte, lista, tuple, dict, frozendict, OrderedDict, set, frozenset, etc.

Låt oss se det i aktion:

python x = '1234567' deep_getsizeof (x, set ()) 44

En sträng av längd 7 tar 44 byte (37 overhead + 7 byte för varje tecken).

python deep_getsizeof ([], set ()) 72

En tom lista tar 72 byte (bara överliggande).

python deep_getsizeof ([x], set ()) 124

En lista som innehåller strängen x tar 124 byte (72 + 8 + 44).

python deep_getsizeof ([x, x, x, x, x], set ()) 156

En lista som innehåller strängen x 5 gånger tar 156 byte (72 + 5 * 8 + 44).

Det sista exemplet visar att deep_getsizeof () räknar referenser till samma objekt (x-strängen) en gång, men varje referens pekare räknas.

Behandlar eller triksar

Det visar sig att CPython har flera knep upp på ärmen, så de siffror du får från deep_getsizeof () representerar inte fullt ut minnesanvändningen av ett Python-program.

Referensräkning

Python hanterar minne med referensräknande semantik. När ett objekt inte längre är hänvisat, är dess minne allokerat. Men så länge som det finns en referens kommer objektet inte att fördelas. Saker som cykliska referenser kan bita dig ganska hårt.

Små föremål

CPython hanterar små objekt (mindre än 256 byte) i specialpooler på 8-byte gränser. Det finns pooler för 1-8 byte, 9-16 byte och hela vägen till 249-256 byte. När ett objekt av storlek 10 tilldelas tilldelas det från 16-byte-poolen för objekt 9-16 byte i storlek. Så, även om det bara innehåller 10 byte data, kommer det att kosta 16 byte minne. Om du fördelar 1 000 000 objekt med storlek 10, använder du faktiskt 16 000 000 byte och inte 10 000 000 byte som du antar. Denna 60% -kostnad är uppenbarligen inte trivial.

heltal

CPython håller en global lista över alla heltal inom intervallet [-5, 256]. Denna optimeringsstrategi är meningsfull eftersom små heltal dyker upp överallt och eftersom varje heltal tar 24 byte sparar det mycket minne för ett typiskt program.

Det betyder också att CPython fördelar 266 * 24 = 6384 byte för alla dessa heltal, även om du inte använder de flesta av dem. Du kan verifiera det med hjälp av id () -funktionen som ger pekaren till det aktuella objektet. Om du kallar id (x) multipel för någon x inom intervallet [-5, 256] får du samma resultat varje gång (för samma heltal). Men om du försöker det för heltal utanför detta intervall kommer var och en att vara annorlunda (ett nytt objekt skapas på flugan varje gång).

Här är några exempel inom intervallet:

"python-id (-3) 140251817361752

id (-3) 140251817361752

id (-3) 140251817361752

id (201) 140251817366736

id (201) 140251817366736

id (201) 140251817366736 "

Här är några exempel utanför intervallet:

"python id (301) 140251846945800

id (301) 140251846945776

id (-6) 140251846946960

id (-6) 140251846946936 "

Python Memory vs System Memory

CPython är typ av possessiv. I många fall, när minnesobjekt i ditt program inte längre refereras, är de inte återvände till systemet (t ex de lilla objekten). Det här är bra för ditt program om du fördelar och fördelar många objekt (som tillhör samma 8-byte-pool) eftersom Python inte behöver störa systemet, vilket är relativt dyrt. Men det är inte så bra om ditt program normalt använder X bytes och under något tillfälligt tillstånd använder det 100 gånger så mycket (t ex analyserar och bearbetar en stor konfigurationsfil endast när den startar).

Nu kan det 100 x minnet fångas oanvändbart i ditt program, aldrig att användas igen och förneka systemet från att fördela det till andra program. Ironin är att om du använder bearbetningsmodulen för att köra flera instanser av ditt program, kommer du allvarligt att begränsa antalet instanser du kan köra på en given maskin.

Memory Profiler

För att mäta och mäta den faktiska minnesanvändningen av ditt program kan du använda modulen memory_profiler. Jag spelade lite med det och jag är inte säker på att jag litar på resultaten. Att använda det är väldigt enkelt. Du dekorerar en funktion (kan vara den viktigaste (0-funktionen) med @profiler-dekoratorn, och när programmet avslutas, skriver minnesprofilen ut till en vanlig rapport, en praktisk rapport som visar totalvärdet och ändras i minnet för varje rad. Här är ett urval program jag sprang under profiler:

"python från memory_profiler importprofil

@profile def main (): a = [] b = [] c = [] för jag i intervallet (100000): a.append (5) för jag i intervallet (100000): b.append (300) för jag i intervall (100000): c.append ('123456789012345678901234567890') del av del c

skriv ut 'klar!' om __name__ == '__main__': main () "

Här är utgången:

Linje # Mem användning Ökning Linjeinnehåll ============================================== ===== 3 22,9 MiB 0,0 MiB @profile 4 def main (): 5 22,9 MiB 0,0 MiB a = [] 6 22,9 MiB 0,0 MiB b = [] 7 22,9 MiB 0,0 MiB c = [] 8 27,1 MiB 4,2 MiB för jag i intervallet (100000): 9 27,1 MiB 0,0 MiB a.append (5) 10 27,5 MiB 0,4 MiB för jag i intervallet (100000): 11 27,5 MiB 0,0 MiB b.append (300) 12 28,3 MiB 0,8 ​​MiB för i området (100000): 13 28,3 MiB 0,0 MiB c.append ('123456789012345678901234567890') 14 27,7 MiB -0,6 MiB del a 15 27,9 MiB 0,2 MiB del b 16 27,3 MiB -0,6 MiB del c 17 18 27,3 MiB 0,0 MiB print ' Gjort!' 

Som du kan se finns det 22,9 MB minnehögtalare. Anledningen till att minnet inte ökar när man lägger till heltal både inom och utanför [-5, 256] -intervallet och även när man lägger till strängen är att ett enda objekt används i alla fall. Det är inte klart varför den första slingan av raden (100000) på rad 8 lägger till 4,2 MB medan den andra på rad 10 lägger till bara 0,4 MB och den tredje slingan på rad 12 lägger till 0,8 MB. Slutligen, när du raderar a, b och c-listorna, släpps -0,6 MB för a och c, men för b är 0,2 MB tillagt. Jag kan inte ge stor mening utifrån dessa resultat.

Slutsats

CPython använder mycket minne för sina objekt. Det använder olika knep och optimeringar för minneshantering. Genom att hålla reda på ditt objekts minnesanvändning och vara medveten om minneshanteringsmodellen kan du avsevärt minska minnets fotavtryck av ditt program.

Lär Python

Lär dig Python med vår kompletta handledning för pythonhandledning, oavsett om du bara har börjat eller du är en erfaren kodare som vill lära dig nya färdigheter.