Hvordan debugger man effektivt hukommelses-problemer i Python?

Jeg har kodet en del i Python de sidste mange år samt C/C++. Jeg fik dog et spørgsmål på bordet den anden dag, som er af generel interesse - hvordan debugger man effektivt efter hukommelsesproblemer i Python på en Linux-maskine? Hvis man f.eks. ser at et Python pludselig er gået fra 100 MB til 5 GB hukommelsesforbrug. Jeg vil gerne høre jeres gode ideer til at jage dette.

Havde det været et C/C++ program havde jeg kørt "valgrind --tool=massif PROGRAM" og derefter "ms_print massif.out.*".Se f.eks. her. Det giver et glimrende billede ind i "hvad/hvor" problemerne stammer fra.

Jeg har ikke til Python fundet et tilsvarende sejt værktøj. Jeg ved ikke om man kan køre Cython på Python-koden, oversætte det hele og derefter "valgrind --tool=massif...". Det ligger ikke lige til højrebenet, men det er noget jeg vil prøve.

Mht. ren Python-kode så faldt jeg over "Meliae", som ser interessant ud. Det fungerer glimrende og man kan med minimale ændringer få oversigter over hvilke typer elementer optager hvor meget hvor meget hukommelse. Det er relevant, men ikke godt nok. Jeg savner to ting. Dels at man kan få "snapshots" af hukommelsen over tid, men også hvilke "dicts" er det værste.

Total 17916 objects, 96 types, Total size = 1.5MiB (1539583 bytes)
Index   Count   %      Size   % Cum     Max Kind
   0     701   3    546460  35  35   49292 dict
   1    7138  39    414639  26  62    4858 str
   2     208   1     94016   6  68     452 type
   3    1371   7     93228   6  74      68 code
   4    1431   7     85860   5  80      60 function
   5    1448   8     59808   3  84     280 tuple
   6     552   3     40760   2  86     684 list
   7      56   0     29152   1  88     596 StgDict
   8    2167  12     26004   1  90      12 int
   9     619   3     24760   1  91      40 wrapper_descriptor
  10     570   3     20520   1  93      36 builtin_function_or_method

Min ven Finn pegede mig til dette link.
Ideen er at indføje "@profile" til de funktioner man mistænker er dårlige og så kører man Python med argument "-m memory_profiler".
Det er også mere interessant, men det er umiddelbart irriterende at man skal fjerne/indsætte "@profile" i koden alt efter om man vil profilere.

Som testprogram kan f.eks. anvendes

def my_func():
    a = [1] * (12 ** 4)
    return a
 
def my_func2():
    b = [2] * (10 ** 7)
    return b
 
if <strong>name</strong> == '<strong>main</strong>':
    a1 = my_func()
    a2 = my_func2()
    # Hvad er slemt? a1 eller a2?

I eksemplet er det nemt af afgøre at my_func2() koster en del, men hvordan gør man det i praksis, når der er 10.000 linier kode?

Har I erfaringer med dette område af Python, så skriv meget gerne om dette nedenfor.

/pto

Kommentarer (12)
sortSortér kommentarer
  • Ældste først
  • Nyeste først
  • Bedste først
Andreas Bach Aaen

Hvis der er nogen der har debug erfaringer med indlejret Pythin kode, så kom frem. Selv når an søger på Stackoverflow, så finder man mange der laver c-moduler til python, men ufatteligt få der gør det den anden vej rundt.

Jeg har haft meget stor glæde af at udvidde en større mængde legacy c-kode, med nye interfaces, der betjenes af Python. REST-baserede interfaces er altså langt nemmere at kode i Python på relativt langt færre linjer end der ville være at kode det i c.
Debugmæssigt kræves der så en ny strategi.

En god del af det nye interface består i at høste data i det eksiterende c-interface og så i c opbygge strukturer, der sendes ind i Python. Her skal man være omhyggelig med Py_DECREF() for ikke at ende med enorme memory leaks.

Et forslag her kunne være at benytte Coccinelle, til at beskrive hvornår Py_DECREF() bør være til stede.

  • 3
  • 0
Anders Munch

random.sample([x for x in gc.get_objects() if isinstance(x,dict)], 20)
kombineret med lidt gc.get_referrers sporing, og du skal nok finde ud af hvad det er for nogle dicts der fylder op. Før eller senere.

Hvis problemet har med noget cyklisk garbage at gøre - og det vil det typisk have i Python - så er en mulighed at bryde referencecyklerne. Det kan man gøre uden at have fundet den helt præcise grund til problemet, når blot man kender en deltager. Fx havde jeg et blæksprutte-objekt (objekt/relationel mapper for en database), som ikke ville forsvinde, selv om jeg mente at der ikke var flere referencer. Så jeg tilføjede en 'luk' metode, som satte alle attributter til None, og kaldte den som det sidste. Det hjalp.

  • 4
  • 0
Anders Munch
  • 1
  • 0
Anders Munch

Hvis du vil have svaret på spørgsmålet, "hvor kommer alle de dict's fra?", så må du jo gå og kigge på de dict's programmet genererer. Se hvad de indeholder, og evt. spore tilbage på hvad det er der refererer til dem, og bruge det til at gætte, hvor i koden de kommer fra. Du kan selvfølgelig ikke nærstudere alle 49292 eksemplarer, men ved at tage et tilfældigt udpluk, så får du formentlig nogen med der er en del af lækken.

Det kræver bare en REPL prompt, så kør programmet en tid og gå så over i et PyCrust vindue (hvis det er en wx GUI app), eller kør python -i mitprogram.py så du får et fortolker-prompt efter programmet er afsluttet.

I virkeligheden ville jeg nok ikke kigge efter dict's først, men efter instanser af dine egne klasser; for dem burde der jo nærmest ikke være nogen tilbage af, med undtagelse af lidt caching og sådan. Så hvis fx mitprogram.py indeholder

import heleverden  
hv = heleverden.HeleVerden()  
hv.kør()

og du så efter python -i mitprogram.py udfører

del hv

så burde der jo ikke være ét eneste objekt tilbage af typen HeleVerden. Ergo må der være noget der har en reference til objektet, som holder det i live. Og gc.get_referrers() vil kunne fortælle hvad:

import gc  
hv = [x for x in gc.get_objects() if ininstance(x, HeleVerden)][0]  
refs = gc.get_referrers(hv)  
print refs

Det er et stort arbejde at finde ud af noget på den måde, så hjælpeværktøjer som Meliae er gode at have. Jeg ville bare gøre opmærksom på, at du behøver ikke nøjes med statistik fra værktøjer, om nødvendigt kan du gå direkte til kilden med gc modulet i CPython, og se på objekterne direkte med dir() og type() og vars() osv.

  • 3
  • 0
Andreas Bach Aaen

Med god grund. Det er bare nemmere at udvide end at indlejre. Princippet er simpelt: Det sprog du ønsker at skrive mest kode i, skal være top-level.

Nu har min opgave været at lave et nyt interface til en meget stor mængde legacy c-kode. Så selv om jeg skriver flere tusinde linjer python, så vil c stadig være det dominerende. Python er blot mere velegnet til at skrive moderne tekst-rige interfaces med.
Man kan godt få json biblioteker til c, men det er ikke den samme fornøjelse som det er i Python.
Valget om det Python indlejret i c eller omvendt kommer også meget nemt til at afhænge af hvem der skal sidde med det centrale select kald - altså ventepositionen for at kommunikation med andre processer. Der er ikke noget værre end at have to ventepositioner og ende med en stump kode der venter lidt her og lidt der.

  • 0
  • 0
Log ind eller Opret konto for at kommentere