Begrijp hoeveel geheugen je Python-objecten gebruiken

Python is een fantastische programmeertaal. Het staat ook bekend als behoorlijk traag, voornamelijk vanwege de enorme flexibiliteit en dynamische functies. Voor veel toepassingen en domeinen is het geen probleem vanwege hun vereisten en verschillende optimalisatietechnieken. Het is minder bekend dat Python-objectgrafieken (geneste woordenboeken van lijsten en tupels en primitieve typen) een aanzienlijke hoeveelheid geheugen innemen. Dit kan een veel ernstigere beperkende factor zijn vanwege de effecten op caching, virtueel geheugen, multi-tenancy met andere programma's en in het algemeen sneller het beschikbare geheugen uitputten, wat een schaars en duur hulpmiddel is.

Het blijkt dat het niet triviaal is om erachter te komen hoeveel geheugen daadwerkelijk wordt verbruikt. In dit artikel zal ik je door de fijne kneepjes van het geheugenbeheer van Python laten zien en laten zien hoe je het verbruikte geheugen nauwkeurig kunt meten.

In dit artikel richt ik me uitsluitend op CPython - de primaire implementatie van de Python-programmeertaal. De experimenten en conclusies hier zijn niet van toepassing op andere Python-implementaties zoals IronPython, Jython en PyPy.

Ook heb ik de getallen op 64-bit Python 2.7 uitgevoerd. In Python 3 zijn de cijfers soms een beetje anders (vooral voor strings die altijd Unicode zijn), maar de concepten zijn hetzelfde.

Hands-On onderzoek naar het gebruik van het Python-geheugen

Laten we eerst een beetje verkennen en een concreet beeld krijgen van het feitelijke geheugengebruik van Python-objecten.

De sysizeizeof () Ingebouwde functie

De sys-module van de standaardbibliotheek biedt de functie getsizeof (). Die functie accepteert een object (en optionele standaard), roept het object De grootte van() methode en retourneert het resultaat, zodat u uw objecten ook inspecteerbaar kunt maken.

Het meten van het geheugen van Python-objecten

Laten we beginnen met een aantal numerieke types:

"Python import sys

sys.getsizeof (5) 24 "

Interessant. Een geheel getal neemt 24 bytes in beslag.

python sys.getsizeof (5.3) 24

Hmm ... een dobber neemt ook 24 bytes in beslag.

python from decimal import Decimal sys.getsizeof (Decimal (5.3)) 80

Wauw. 80 bytes! Dit laat je echt nadenken over de vraag of je een groot aantal reële getallen wilt weergeven als drijvers of decimalen.

Laten we verder gaan naar tekenreeksen en verzamelingen:

"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. Een lege string kost 37 bytes en elk extra teken voegt een andere byte toe. Dat zegt veel over de afwegingen van het bijhouden van meerdere korte strings waarbij je de 37 bytes overhead betaalt voor elke versus een enkele lange string, waarbij je de overhead slechts één keer betaalt.

Unicode-strings gedragen zich op dezelfde manier, behalve dat de overhead 50 bytes is en elk extra teken 2 bytes toevoegt. Dat is iets om rekening mee te houden als u bibliotheken gebruikt die Unicode-reeksen retourneren, maar uw tekst kan worden weergegeven als eenvoudige tekenreeksen.

Trouwens, in Python 3 zijn strings altijd Unicode en de overhead is 49 bytes (ze hebben ergens een byte opgeslagen). Het bytes-object heeft een overhead van slechts 33 bytes. Als je een programma hebt dat veel korte strings in het geheugen verwerkt en je geeft om prestaties, denk dan eens aan Python 3.

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

Wat gebeurd er? Een lege lijst kost 72 bytes, maar elke extra int voegt slechts 8 bytes toe, waarbij de grootte van een int 24 bytes is. Een lijst met een lange reeks neemt slechts 80 bytes in beslag.

Het antwoord is simpel. De lijst bevat niet de int-objecten zelf. Het bevat alleen een 8-byte (op 64-bit versies van CPython) aanwijzer naar het eigenlijke int-object. Wat dat betekent is dat de functie getsizeof () niet het daadwerkelijke geheugen van de lijst en alle objecten die het bevat, maar alleen het geheugen van de lijst en de verwijzingen naar de objecten ervan retourneert. In de volgende sectie zal ik de functie deep_getsizeof () introduceren die dit probleem aanpakt.

python sys.getsizeof (()) 56 sys.getsizeof ((1,)) 64 sys.getsizeof ((1, 2, 3, 4)) 88 sys.getsizeof ((een lange longlong-reeks),)) 64

Het verhaal is vergelijkbaar voor tuples. De overhead van een lege tuple is 56 bytes versus de 72 van een lijst. Nogmaals, dit verschil van 16 bytes per reeks is laaghangend fruit als je een gegevensstructuur hebt met veel kleine, onveranderlijke reeksen.

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

Sets en woordenboeken zullen ogenschijnlijk helemaal niet groeien als je items toevoegt, maar let op de enorme overhead.

De bottom line is dat Python-objecten een enorme vaste overhead hebben. Als uw gegevensstructuur is samengesteld uit een groot aantal verzamelobjecten zoals strings, lijsten en woordenboeken die elk een klein aantal items bevatten, betaalt u een zware tol.

De functie deep_getsizeof ()

Nu ik je half dood heb laten schrikken en ook heb aangetoond dat sys.getsizeof () je alleen kan vertellen hoeveel geheugen een primitief object heeft, laten we eens kijken naar een adequatere oplossing. De functie deep_getsizeof () boort recursief af en berekent het werkelijke geheugengebruik van een Python-objectgrafiek.

"python from collections import Mapping, Container from sys import getsizeof

def deep_getsizeof (o, ids): "" "Zoek de geheugenvoetafdruk van een Python-object

Dit is een recursieve functie die een Python-objectgrafiek analyseert zoals een woordenboek met geneste woordenboeken met lijsten met lijsten en tupels en sets. De sysizeizeof functie heeft slechts een ondiepe afmeting. Het telt elk object in een container als aanwijzer, ongeacht hoe groot het werkelijk is. : param o: het object: param ids:: return: "" "d = deep_getsofof if id (o) in id's: return 0 r = getsizeof (o) ids.add (id (o)) if isinstance (o, str ) of isinstance (0, unicode): return r if isinstance (o, Mapping): return r + sum (d (k, id's) + d (v, id's) voor k, v in o.iteritems ()) als isinstance (o, Container): return r + sum (d (x, id's) voor x in o) return r "

Er zijn verschillende interessante aspecten aan deze functie. Het houdt rekening met objecten waarnaar meerdere keren wordt verwezen en telt deze slechts één keer door object-id's bij te houden. Het andere interessante kenmerk van de implementatie is dat het volledig profiteert van de abstracte basisklassen van de verzamelingsmodule. Dat maakt de functie zeer beknopt om elke verzameling aan te pakken die de basisklassen Mapping of Container implementeert in plaats van direct met ontelbare collectietypen om te gaan, zoals: string, Unicode, bytes, lijst, tuple, dict, frozendict, OrderedDict, set, frozenset, enz..

Laten we het in actie zien:

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

Een reeks van lengte 7 neemt 44 bytes (37 overhead + 7 bytes voor elk teken).

python deep_getsizeof ([], set ()) 72

Een lege lijst kost 72 bytes (alleen overhead).

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

Een lijst die de string x bevat, neemt 124 bytes (72 + 8 + 44).

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

Een lijst die de string x 5 keer bevat, kost 156 bytes (72 + 5 * 8 + 44).

Het laatste voorbeeld laat zien dat deep_getsizeof () verwijzingen naar hetzelfde object (de x-tekenreeks) slechts eenmaal telt, maar de aanwijzer van elke verwijzing wordt geteld.

Treats of Tricks

Het blijkt dat CPython verschillende tricks op zijn naam heeft staan, dus de getallen die je krijgt van deep_getsizeof () vertegenwoordigen niet volledig het geheugengebruik van een Python-programma.

Referentietelling

Python beheert het geheugen met behulp van referentietellende semantiek. Als er niet meer naar een object wordt verwezen, wordt zijn geheugen de toewijzing ongedaan gemaakt. Maar zolang er een referentie is, zal het object niet worden verwijderd. Zaken als cyclische verwijzingen kunnen je behoorlijk bijten.

Kleine voorwerpen

CPython beheert kleine objecten (minder dan 256 bytes) in speciale pools op 8-bytegrenzen. Er zijn pools voor 1-8 bytes, 9-16 bytes en helemaal tot 249-256 bytes. Wanneer een object van formaat 10 is toegewezen, wordt dit toegewezen aan de 16-bytespool voor objecten van 9-16 bytes. Dus hoewel het maar 10 bytes aan gegevens bevat, kost het 16 bytes aan geheugen. Als u 1.000.000 objecten van grootte 10 toewijst, gebruikt u feitelijk 16.000.000 bytes en geen 10.000.000 bytes, zoals u wellicht aanneemt. Deze overhead van 60% is uiteraard niet triviaal.

integers

CPython houdt een algemene lijst bij van alle gehele getallen in het bereik [-5, 256]. Deze optimalisatiestrategie is logisch, omdat hele kleine getallen overal opduiken, en aangezien elk geheel getal 24 bytes kost, bespaart het veel geheugen voor een typisch programma.

Het betekent ook dat CPython al deze gehele getallen 266 * 24 = 6384 bytes toewijst, zelfs als u de meeste niet gebruikt. U kunt het verifiëren door de id () -functie te gebruiken die de aanwijzer naar het werkelijke object geeft. Als u id (x) multiple callt voor elke x in het bereik [-5, 256], krijgt u telkens hetzelfde resultaat (voor hetzelfde gehele getal). Maar als u het voor gehele getallen buiten dit bereik probeert, zal elk ervan anders zijn (een nieuw object wordt elke keer on-the-fly aangemaakt).

Hier zijn een paar voorbeelden binnen het bereik:

"python id (-3) 140251817361752

id (-3) 140251817361752

id (-3) 140251817361752

id (201) 140251817366736

id (201) 140251817366736

id (201) 140251817366736 "

Hier zijn enkele voorbeelden buiten het bereik:

"python id (301) 140251846945800

id (301) 140251846945776

id (-6) 140251846946960

id (-6) 140251846946936 "

Python-geheugen versus systeemgeheugen

CPython is nogal bezitterig. In veel gevallen, wanneer er niet meer naar geheugenobjecten in uw programma wordt verwezen, zijn ze dat wel niet teruggekeerd naar het systeem (bijvoorbeeld de kleine voorwerpen). Dit is goed voor je programma als je veel objecten (die tot dezelfde 8-bytespool behoren) toewijst en de toewijzing opgeeft, omdat Python het systeem niet hoeft lastig te vallen, wat relatief duur is. Maar het is niet zo geweldig als uw programma normaal X-bytes gebruikt en in een tijdelijke toestand 100 keer zoveel gebruikt (bijvoorbeeld het ontleden en verwerken van een groot configuratiebestand alleen als het wordt gestart).

Nu kan dat 100X-geheugen nutteloos in uw programma worden opgesloten, nooit meer opnieuw worden gebruikt en het systeem niet toestaan ​​het aan andere programma's toe te wijzen. Het ironische is dat als je de verwerkingsmodule gebruikt om meerdere instanties van je programma uit te voeren, je het aantal exemplaren dat je op een bepaalde machine kunt uitvoeren aanzienlijk beperkt.

Memory Profiler

Om het werkelijke geheugengebruik van uw programma te meten en te meten, kunt u de module memory_profiler gebruiken. Ik speelde er een beetje mee en ik weet niet zeker of ik de resultaten vertrouw. Het gebruiken ervan is heel eenvoudig. Je decoreert een functie (zou de hoofdfunctie (0 functie) kunnen zijn met @profiler-decorator en wanneer het programma wordt afgesloten, drukt de geheugenprofiler af naar standaarduitvoer een handig rapport dat het totaal en de wijzigingen in het geheugen voor elke regel weergeeft. programma liep ik onder de profiler:

"python from memory_profiler importprofiel

@profile def main (): a = [] b = [] c = [] voor i in bereik (100000): a.append (5) voor i in bereik (100000): b.append (300) voor i in bereik (100000): c.append ('123456789012345678901234567890') del a del b del c

print 'Klaar!' if __name__ == '__main__': main () "

Dit is de uitvoer:

Line # Mem gebruik Verhoogde regelinhoud ======================= ===== 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 voor i in bereik (100000): 9 27,1 MiB 0,0 MiB a.append (5) 10 27,5 MiB 0,4 MiB voor i binnen bereik (100000): 11 27,5 MiB 0,0 MiB b.append (300) 12 28,3 MiB 0,8 ​​MiB voor i in bereik (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 afdrukken ' Gedaan!' 

Zoals u ziet, is er 22,9 MB geheugenoverhead. De reden dat het geheugen niet toeneemt bij het toevoegen van gehele getallen zowel binnen als buiten het bereik [-5, 256] en ook bij het toevoegen van de tekenreeks, is dat een enkel object in alle gevallen wordt gebruikt. Het is niet duidelijk waarom de eerste lus van bereik (100000) op regel 8 4,2 MB toevoegt, terwijl de tweede op lijn 10 slechts 0,4 MB toevoegt en de derde lus op regel 12 0,8 MB toevoegt. Ten slotte wordt bij het verwijderen van de a-, b- en c-lijsten -0.6MB vrijgegeven voor a en c, maar voor b 0.2MB wordt toegevoegd. Ik kan niet veel logisch zijn van deze resultaten.

Conclusie

CPython gebruikt veel geheugen voor zijn objecten. Het maakt gebruik van verschillende trucs en optimalisaties voor geheugenbeheer. Door het geheugengebruik van uw object bij te houden en op de hoogte te zijn van het geheugenbeheermodel, kunt u de geheugenvoetafdruk van uw programma aanzienlijk verminderen.

Leer Python

Leer Python met onze complete python-handleiding, of je nu net begint of dat je een ervaren coder bent die op zoek is naar nieuwe vaardigheden.