Python Memory Management: The Essential Guide

Arwin Lashawn on December 04, 2020

Baggrund

Python er ikke kendt for at være et “hurtigt” programmeringssprog. Men ifølge resultaterne af Stack Overflow Developer Survey 2020 er Python det næstmest populære programmeringssprog efter JavaScript (som du måske har gættet). Dette skyldes i høj grad dets supervenlige syntaks og dets anvendelighed til stort set alle formål. Selv om Python ikke er det hurtigste sprog derude, har dets store læsbarhed kombineret med uovertruffen fællesskabsstøtte og tilgængelighed af biblioteker gjort det ekstremt attraktivt til at få ting gjort med kode.

Pythons hukommelsesstyring spiller også en rolle for dets popularitet. Hvordan det? Pythons hukommelsesstyring er implementeret på en måde, der gør vores liv lettere. Har du nogensinde hørt om Pythons hukommelseshåndtering? Det er den manager, der holder styr på Pythons hukommelse, så du kan fokusere på din kode i stedet for at skulle bekymre dig om hukommelsesstyring. På grund af sin enkelhed giver Python dig dog ikke meget frihed til at styre hukommelsesforbruget, i modsætning til sprog som C++, hvor du manuelt kan allokere og frigøre hukommelse.

Men at have en god forståelse af Pythons hukommelsesstyring er en god start, som vil gøre dig i stand til at skrive mere effektiv kode. I sidste ende kan du håndhæve det som en vane, der potentielt kan overtages i andre programmeringssprog, som du kender.

Så hvad får vi ud af at skrive hukommelseseffektiv kode?

  • Det fører til hurtigere behandling og mindre behov for ressourcer, nemlig random access memory (RAM) brug. Mere tilgængelig RAM vil generelt betyde mere plads til cache, hvilket vil bidrage til at fremskynde diskadgang. Det gode ved at skrive kode, der er hukommelseseffektiv, er, at det ikke nødvendigvis kræver, at du skal skrive flere linjer kode.
  • En anden fordel er, at det forhindrer hukommelseslækage, et problem, der får RAM-forbruget til at stige kontinuerligt, selv når processer dræbes, hvilket i sidste ende fører til langsommere eller forringet enhedspræstation. Dette skyldes, at der ikke frigives brugt hukommelse, efter at processerne er afsluttet.

I den tekniske verden har du måske hørt, at “gjort er bedre end perfekt”. Men lad os sige, at du har to udviklere, der har brugt Python til at udvikle den samme app, og at de har afsluttet den inden for samme tid. Den ene af dem har skrevet en mere hukommelseseffektiv kode, som resulterer i en app, der fungerer hurtigere. Ville du hellere vælge den app, der kører problemfrit, eller den, der kører mærkbart langsommere? Dette er et godt eksempel, hvor to personer ville bruge den samme mængde tid på at kode og alligevel have mærkbart forskellige kodepræstationer.

Her er, hvad du vil lære i guiden:

  • Hvordan håndteres hukommelse i Python?
  • Python Garbage Collection
  • Monitoring Python Memory Issue
  • Best Practices for Improving Python Code Performance

Hvordan forvaltes hukommelse i Python?

I henhold til Pythons dokumentation (3.9.0) for hukommelsesstyring omfatter Pythons hukommelsesstyring en privat heap, der bruges til at gemme dit programs objekter og datastrukturer. Husk også, at det er Pythons hukommelseshåndtering, der håndterer det meste af det beskidte arbejde i forbindelse med hukommelseshåndtering, så du bare kan fokusere på din kode.

Python-hukommelsesallokering

Alt i Python er et objekt. For at disse objekter kan være nyttige, skal de gemmes i hukommelsen for at kunne tilgås. Før de kan lagres i hukommelsen, skal der først allokeres eller tildeles et stykke hukommelse til hvert af dem.

På det laveste niveau sørger Pythons rå hukommelsesallokator først for, at der er ledig plads i den private heap til at lagre disse objekter. Det gør den ved at interagere med hukommelseshåndteringen i dit styresystem. Betragt det som dit Python-program, der anmoder dit operativsystem om et stykke hukommelse til at arbejde med.

På det næste niveau opererer flere objektspecifikke allokatorer på den samme heap og implementerer forskellige forvaltningspolitikker afhængigt af objekttypen. Som du måske allerede ved, er nogle eksempler på objekttyper strings og integers. Selv om strings og integers måske ikke er så forskellige, når man tænker på, hvor meget tid vi bruger på at genkende og huske dem, behandles de meget forskelligt af computere. Det skyldes, at computere har brug for forskellige lagerkrav og hastighedsafvejninger for integers sammenlignet med strings.

En sidste ting du bør vide om, hvordan Pythons heap forvaltes, er, at du har nul kontrol over den. Nu undrer du dig måske over, hvordan vi så kan skrive hukommelseseffektiv kode, hvis vi har så lidt kontrol over Pythons hukommelsesstyring? Før vi kommer ind på det, skal vi yderligere forstå nogle vigtige termer vedrørende hukommelsesstyring.

Statisk vs. dynamisk hukommelsesallokering

Nu hvor du forstår, hvad hukommelsesallokering er, er det tid til at gøre dig bekendt med de to typer af hukommelsesallokering, nemlig statisk og dynamisk, og skelne mellem de to.

Statisk hukommelsesallokering:

  • Som ordet “statisk” antyder, er statisk allokerede variabler permanente, hvilket betyder, at de skal allokeres på forhånd og varer så længe programmet kører.
  • Hukommelse allokeres under kompileringstiden eller før programmets udførelse.
  • Implementeres ved hjælp af stackdatastrukturen, hvilket betyder, at variabler gemmes i stackhukommelsen.
  • Hukommelse, der er allokeret, kan ikke genbruges, og der er således ingen mulighed for genbrug af hukommelse.

Dynamisk hukommelsesallokering:

  • Som ordet “dynamisk” antyder, er dynamisk allokerede variabler ikke permanente og kan allokeres, mens et program kører.
  • Hukommelse allokeres ved kørselstid eller under programudførelsen.
  • Implementeret ved hjælp af heap-datastrukturen, hvilket betyder, at variabler gemmes i heap-hukommelsen.
  • Hukommelse, der er blevet allokeret, kan frigives og genbruges.

En fordel ved dynamisk hukommelsesallokering i Python er, at vi ikke på forhånd behøver at bekymre os om, hvor meget hukommelse vi har brug for til vores program. En anden fordel er, at manipulation af datastrukturer kan foregå frit uden at skulle bekymre sig om et behov for en højere hukommelsesallokering, hvis datastrukturen udvides.

Men da dynamisk hukommelsesallokering foretages under programudførelsen, vil det bruge mere tid på at gennemføre programmet. Desuden skal den hukommelse, der er blevet allokeret, frigives, når den er blevet brugt. Ellers kan der potentielt opstå problemer som f.eks. hukommelseslækager.

Vi stødte på to typer hukommelsesstrukturer ovenfor – heap-hukommelse og stack-hukommelse. Lad os se nærmere på dem.

Stack-hukommelse

Alle metoder og deres variabler er gemt i stack-hukommelsen. Husker du, at stackhukommelse allokeres under kompileringstiden? Det betyder i praksis, at adgangen til denne type hukommelse er meget hurtig.

Når en metode kaldes i Python, allokeres en stack frame. Denne stack frame vil håndtere alle variablene i metoden. Når metoden er returneret, destrueres stakrammen automatisk.

Bemærk, at stakrammen også er ansvarlig for at indstille omfanget for en metodes variabler.

Heap-hukommelse

Alle objekter og instansvariabler gemmes i heap-hukommelsen. Når en variabel oprettes i Python, gemmes den i en privat heap, som derefter giver mulighed for allokering og deallokering.

Heap-hukommelsen gør det muligt for disse variabler at få global adgang til dem af alle dit programs metoder. Når variablen er returneret, går Pythons garbage collector i gang, hvis virkemåde vi kommer ind på senere.

Nu skal vi se på Pythons hukommelsesstruktur.

Python har tre forskellige niveauer, når det gælder hukommelsesstrukturen:

  • Arenaer
  • Pools
  • Blokke

Vi starter med den største af dem alle – arenaer.

Arenaer

Forestil dig et skrivebord med 64 bøger, der dækker hele overfladen. Toppen af skrivebordet repræsenterer en arena, der har en fast størrelse på 256KiB, som er allokeret i heap’en (bemærk, at KiB er forskelligt fra KB, men du kan antage, at de er det samme i denne forklaring). En arena repræsenterer den størst mulige klump hukommelse.

Mere specifikt er arenaer hukommelsestilknytninger, der bruges af Pythons allokeringsværktøj, pymalloc, som er optimeret til små objekter (mindre end eller lig med 512 bytes). Arenas er ansvarlige for allokering af hukommelse, og derfor behøver efterfølgende strukturer ikke længere at gøre det.

Denne arena kan derefter opdeles yderligere i 64 pools, som er den næststørste hukommelsesstruktur.

Pools

For at vende tilbage til skrivebordseksemplet repræsenterer bøgerne alle puljerne inden for en arena.

Hver pulje vil typisk have en fast størrelse på 4Kb og kan have tre mulige tilstande:

  • Tomt: Puljen er tom og dermed tilgængelig for tildeling.
  • Brugt: Puljen indeholder objekter, som gør, at den hverken er tom eller fuld.
  • Fuld: Puljen er fuld og er derfor ikke tilgængelig for yderligere tildeling.

Bemærk, at poolens størrelse bør svare til standardhukommelsessidestørrelsen i dit operativsystem.

En pool er derefter opdelt i mange blokke, som er de mindste hukommelsesstrukturer.

Blokke

For at vende tilbage til skrivebordseksemplet repræsenterer siderne i hver bog alle blokkene i en pool.

I modsætning til arenaer og pools er størrelsen af en blok ikke fast. Størrelsen af en blok varierer fra 8 til 512 bytes og skal være et multiplum af otte.

Hver blok kan kun gemme ét Python-objekt af en bestemt størrelse og har tre mulige tilstande:

  • Uberørt: Er ikke blevet allokeret
  • Fri: Er blevet allokeret, men er blevet frigivet og gjort tilgængelig for allokering
  • Allokeret:

Bemærk, at de tre forskellige niveauer af en hukommelsesstruktur (arenaer, puljer og blokke), som vi diskuterede ovenfor, er specielt beregnet til mindre Python-objekter. Store objekter henvises til standard C allocator i Python, hvilket ville være god læsning for en anden dag.

Python Garbage Collection

Garbage collection er en proces, der udføres af et program for at frigive tidligere allokeret hukommelse til et objekt, der ikke længere er i brug. Du kan tænke på garbage allocation som genbrug eller genanvendelse af hukommelse.

I gamle dage skulle programmører manuelt allokere og deallokere hukommelse. Hvis man glemte at deallokere hukommelse, kunne det føre til en hukommelseslækage, hvilket førte til et fald i eksekveringspræstationen. Værre er det, at manuel allokering og deallokering af hukommelse endda kan føre til utilsigtet overskrivning af hukommelse, hvilket kan få programmet til at gå helt ned.

I Python sker garbage collection automatisk og sparer dig derfor for en masse hovedpine ved manuelt at administrere allokering og deallokering af hukommelse. Python bruger specifikt referencetælling kombineret med generationsbaseret garbage collection til at frigøre ubrugt hukommelse. Grunden til, at referencetælling alene ikke er tilstrækkeligt for Python, er, at det ikke effektivt rydder op i dinglende cykliske referencer.

En generational garbage collection-cyklus indeholder følgende trin –

  1. Python initialiserer en “discard-liste” for ubrugte objekter.
  2. Der køres en algoritme for at opdage referencecyklusser.
  3. Hvis et objekt mangler eksterne referencer, indsættes det i kassationslisten.
  4. Frigør hukommelsesallokering til objekterne i “discard-listen”.

For at lære mere om garbage collection i Python, kan du læse vores Python Garbage Collection: A Guide for Developers indlæg.

Overvågning af hukommelsesproblemer i Python

Selv om alle elsker Python, er det ikke bange for at have hukommelsesproblemer. Der er mange mulige årsager.

I henhold til Pythons (3.9.0) dokumentation for hukommelseshåndtering frigiver Pythons hukommelseshåndtering ikke nødvendigvis hukommelsen tilbage til dit operativsystem. Det fremgår af dokumentationen, at “under visse omstændigheder udløser Pythons hukommelseshåndtering muligvis ikke passende handlinger, som f.eks. garbage collection, hukommelseskomprimering eller andre forebyggende foranstaltninger.”

Som følge heraf kan man være nødt til eksplicit at frigøre hukommelse i Python. En måde at gøre dette på er at tvinge Pythons garbage collector til at frigive ubrugt hukommelse ved at gøre brug af gc-modulet. Man skal blot køre gc.collect() for at gøre dette. Dette giver dog kun mærkbare fordele, når der manipuleres med et meget stort antal objekter.

Afhængig af Pythons garbage collector, der lejlighedsvis er fejlagtig, især når der er tale om store datasæt, er flere Python-biblioteker også kendt for at forårsage hukommelseslækager. Pandas er f.eks. et af disse værktøjer, der er på radaren. Overvej at tage et kig på alle de hukommelsesrelaterede problemer i pandas’ officielle GitHub-repository!

En indlysende årsag, der kan slippe forbi selv de skarpe øjne hos kodegennemgangere, er, at der er tilbageværende store objekter i koden, som ikke er frigivet. I samme forbindelse er uendeligt voksende datastrukturer en anden årsag til bekymring. For eksempel en voksende ordbogsdatastruktur uden en fast størrelsesgrænse.

En måde at løse den voksende datastruktur på er at konvertere ordbogen til en liste, hvis det er muligt, og fastsætte en maksimal størrelse for listen. Ellers skal du blot indstille en grænse for ordbogens størrelse og rydde den, når grænsen er nået.

Nu undrer du dig måske over, hvordan jeg overhovedet kan opdage hukommelsesproblemer i første omgang? En mulighed er at drage fordel af et APM-værktøj (Application Performance Monitoring). Derudover kan mange nyttige Python-moduler hjælpe dig med at spore og spore hukommelsesproblemer. Lad os se på vores muligheder og starte med APM-værktøjer.

Application Performance Monitoring (APM) Tools

Så hvad er Application Performance Monitoring helt præcist, og hvordan hjælper det med at opspore hukommelsesproblemer? Et APM-værktøj giver dig mulighed for at observere et programs ydeevne i realtid, hvilket muliggør løbende optimering, efterhånden som du opdager problemer, der begrænser ydeevnen.

Baseret på de rapporter, der genereres af APM-værktøjer, vil du være i stand til at få et overblik over, hvordan dit program fungerer. Da du kan modtage og overvåge præstationsmetrikker i realtid, kan du straks gribe ind over for eventuelle problemer, der observeres. Når du har indsnævret de mulige områder i dit program, der kan være skyld i hukommelsesproblemer, kan du derefter dykke ned i koden og diskutere den med de andre kodebidragydere for yderligere at bestemme de specifikke kodelinjer, der skal rettes.

Det kan være en skræmmende opgave at spore roden til problemer med hukommelseslækager. At løse det er et andet mareridt, da du er nødt til virkelig at forstå din kode. Hvis du nogensinde befinder dig i den situation, skal du ikke lede længere, for ScoutAPM er et dygtigt APM-værktøj, der konstruktivt kan analysere og optimere din applikations ydeevne. ScoutAPM giver dig indsigt i realtid, så du hurtigt kan lokalisere & løse problemer, før dine kunder måske opdager dem.

Profilmoduler

Der er mange praktiske Python-moduler, som du kan bruge til at løse hukommelsesproblemer, uanset om det er en hukommelseslækage eller dit program, der går ned på grund af overdreven hukommelsesforbrug. To af de anbefalede er:

  • tracemalloc
  • memory-profiler
  • Bemærk, at kun modulet tracemalloc er indbygget, så du skal sørge for først at installere det andet modul, hvis du vil bruge det.

    tracemalloc

    I henhold til Python (3.9.0)-dokumentationen for tracemalloc kan du ved at bruge dette modul få følgende oplysninger:

    • Traceback, hvor et objekt blev allokeret.
    • Statistik over allokerede hukommelsesblokke pr. filnavn og pr. linjenummer: samlet størrelse, antal og den gennemsnitlige størrelse af allokerede hukommelsesblokke.
    • Beregne forskellen mellem to øjebliksbilleder for at opdage hukommelseslækager.

    Et anbefalet første skridt, som du bør tage for at finde kilden til hukommelsesproblemet, er først at vise de filer, der allokerer mest hukommelse. Du kan nemt gøre dette ved hjælp af det første kodeeksempel, der er vist i dokumentationen.

    Dette betyder dog ikke, at filer, der allokerer en lille mængde hukommelse, ikke vil vokse i det uendelige og forårsage hukommelseslækager i fremtiden.

    memory_profiler

    Dette modul er et sjovt et. Jeg har arbejdet med dette, og det er en personlig favorit, fordi det giver mulighed for blot at tilføje @profile-dekoratoren til enhver funktion, som du ønsker at undersøge. Det output, der gives som resultat, er også meget let forståeligt.

    En anden grund, der gør dette til min personlige favorit, er, at dette modul giver dig mulighed for at plotte en graf over tidsbaseret hukommelsesforbrug. Nogle gange har du blot brug for en hurtig kontrol for at se, om hukommelsesforbruget fortsætter med at stige på ubestemt tid eller ej. Dette er den perfekte løsning til det, da du ikke behøver at lave en linje-for-linje hukommelsesprofilering for at bekræfte det. Du kan blot observere den plottede graf efter at have ladet profileringen køre i et vist tidsrum. Her er et eksempel på den graf, der udsendes –

    I henhold til beskrivelsen i memory-profiler-dokumentationen er dette Python-modul til overvågning af hukommelsesforbruget i en proces samt en linje-for-linje-analyse af det samme for Python-programmer. Det er et rent Python-modul, der afhænger af biblioteket psutil.

    Jeg anbefaler at læse denne Medium-blog for at udforske yderligere, hvordan memory-profiler bruges. Der vil du også lære, hvordan du bruger et andet Python-modul, muppy (det nyeste er muppy3).

    Best Practices for Improving Python Code Performance

    Godt nok med alle detaljerne om hukommelsesstyring. Lad os nu undersøge nogle af de gode vaner i forbindelse med at skrive hukommelseseffektiv Python-kode.

    Tag fordel af Python-biblioteker og indbyggede funktioner

    Ja, dette er en god vane, som måske ret ofte bliver overset. Python har uovertruffen støtte fra fællesskabet, og det afspejles af de rigelige Python-biblioteker, der er tilgængelige til stort set alle formål, lige fra API-opkald til datalogi.

    Hvis der findes et Python-bibliotek, som gør det muligt at gøre det samme som det, du allerede har implementeret, er det, du kan gøre, at sammenligne din kodeydelse, når du bruger biblioteket sammenlignet med, når du bruger din brugerdefinerede kode. Der er stor sandsynlighed for, at Python-biblioteker (især de populære biblioteker) vil være mere hukommelseseffektive end din kode, fordi de løbende forbedres på baggrund af feedback fra fællesskabet. Vil du hellere stole på kode, der er blevet lavet fra den ene dag til den anden, eller en kode, der er blevet strengt forbedret i en længere periode?

    Det bedste af det hele er, at Python-biblioteker vil spare dig mange linjer kode, så hvorfor ikke?

    Nej at bruge “+” til sammenkædning af strenge

    På et tidspunkt har vi alle været skyldige i at sammenkæde strenge ved hjælp af operatoren “+”, fordi det ser så nemt ud.

    Bemærk, at strenge er uforanderlige. Derfor skal Python, hver gang du tilføjer et element til en streng med “+”-operatoren, oprette en ny streng med en ny hukommelsesallokering. Med længere strenge vil kodens hukommelsesineffektivitet blive mere udtalt.

    Anvendelse af itertools til effektiv looping

    Looping er en væsentlig del af automatisering af ting. Efterhånden som vi fortsætter med at bruge løkker mere og mere, vil vi til sidst finde os selv nødt til at bruge indlejrede løkker, som er kendt for at være ineffektive på grund af deres høje køretidskompleksitet.

    Det er her, at itertools-modulet kommer til undsætning. Ifølge Pythons itertools-dokumentation: “Modulet standardiserer et kernesæt af hurtige, hukommelseseffektive værktøjer, som er nyttige alene eller i kombination. Sammen gør de det muligt at konstruere specialiserede værktøjer kortfattet og effektivt i ren Python.”

    Med andre ord gør itertools-modulet det muligt at lave hukommelseseffektive løkker ved at slippe af med unødvendige løkker. Interessant nok kaldes itertools-modulet for en perle, da det gør det muligt at sammensætte elegante løsninger på et utal af problemer.

    Jeg er ret sikker på, at du kommer til at arbejde med mindst én løkke i dit næste stykke kode, så prøv at implementere itertools så!

    Rekonklusion og afsluttende tanker

    Anvendelse af gode Python-hukommelseshåndteringsvaner er ikke for den tilfældige programmør. Hvis du normalt klarer dig med simple scripts, bør du slet ikke løbe ind i hukommelsesrelaterede problemer. Takket være hardware og software, der fortsat undergår en hurtig udvikling, mens du læser dette, bør basismodellen af stort set alle enheder derude, uanset deres mærker, køre hverdagsprogrammer helt fint. Behovet for hukommelseseffektiv kode begynder først at vise sig, når man begynder at arbejde på en stor kodebase, især til produktion, hvor ydelsen er afgørende.

    Dette betyder dog ikke, at hukommelsesstyring i Python er et svært begreb at forstå, og det betyder heller ikke, at det ikke er vigtigt. Det skyldes, at der lægges mere og mere vægt på applikationspræstationer for hver dag, der går. En dag vil det ikke længere blot være et spørgsmål om “færdig”. I stedet vil udviklerne konkurrere om at levere en løsning, der ikke blot er i stand til at løse kundernes behov med succes, men som også gør det med lynhurtig hastighed og med et minimum af ressourcer.