Anders Jonsson
anders.jonsson@norsjovallen.se 2015

# Använda Massif för att profilera minnesanvändning i GNOME-program

Denna artikel beskriver hur heap-profileraren Massif kan användas med GNOME-program. Vi beskriver hur du kan köra, tolka och reagera på utdata från Massif. Spelet Swell Foop används som ett exempel.

# Introduktion

Massif är en medlem av valgrind-sviten av verktyg för minnesprofilering. Dess syfte är att ge en detaljerad överblick över dynamisk minnesanvändning under programmets livstid. Specifikt lagrar det minnesanvändningen för heapen och stacken.

Heapen är minnesregionen som allokeras med funktioner som malloc. Den växer enligt efterfrågan och är vanligen den största minnesregionen i ett program. Stacken är där alla lokala data för funktioner lagras. Detta inkluderar de ”automatiska” variablerna i C och returadressen för subrutiner. Stacken är vanligen mycket mindre och mycket mer aktiv än heapen. Vi kommer inte explicit tänka närmare på stacken eftersom Massif behandlar den som att den bara vore en annan del av heapen. Massif ger också information om hur mycket minne som används för att hantera heapen.

Massif skapar två utdatafiler: en grafisk överblick i en postscript-fil och en detaljerad sammanställning i en textfil.

# Använda Massif med GNOME

Massif har väldigt få flaggor och behöver dem inte för många program. För GNOME-program, där minnesallokering kan vara gömd djupt nere i antingen glib eller GTK, kan dock antalet nivåer som Massif går ner för anropsstacken behöva ökas. Detta uppnås med parametern --depth. Denna är som standard 3. Att öka den till 5 garanterar att anropsstacken når ner till din kod. En eller två ytterligare nivåer kan också vara önskvärda för att tillhandahålla mer sammanhang till din kod. Eftersom detaljnivån snabbt blir överväldigande är det bäst att börja med den mindre djupparametern och endast öka den då det blir uppenbart att den är otillräcklig.

Det är också användbart att berätta för Massif vilka funktioner som allokerar minne i glib. Det tar bort ett onödigt lager av funktionsanrop från rapporterna och ger dig en tydligare bild av vilken kod som allokerar minne. De allokerande funktionerna i glib är g_malloc, g_malloc0, g_realloc, g_try_malloc och g_mem_chunk_alloc. Du använder flaggan --alloc-fn för att informera Massif om dem.

Din kommandorad bör därför se ut något i stil med:

valgrind --tool=massif --depth=5 --alloc-fn=g_malloc --alloc-fn=g_realloc --alloc-fn=g_try_malloc \ --alloc-fn=g_malloc0 --alloc-fn=g_mem_chunk_alloc swell-foop

Swell Foop är programmet vi kommer använda som ett exempel. Var redo på att eftersom valgrind emulerar processorn så kommer det köra väldigt långsamt. Du kommer också behöva mycket minne.

# Tolka resultaten

Grafiskt utdata för Massif är i stort självförklarande. Varje band representerar minnet allokerat av en funktion över tid. Då du väl identifierat vilka band som använder mest minne, oftast de tjocka banden högst upp, kommer du att behöva läsa textfilen för detaljer.

Textfilen är arrangerad som en hierarki av avsnitt, högst upp är en lista över de värsta minnesanvändarna arrangerade i ordning efter sjunkande utrymmestid. Under detta är vidare avsnitt som vardera delar upp resultaten i större detalj allt efter att du fortsätter ner för anropsstacken. För att illustrera detta kommer vi visa utdata för kommandot ovan.

## Utdata från Massif för den ej optimerade versionen av programmet Swell Foop.

Bilden ovan visar typiskt postscript-utdata från Massif. Detta är resultatet du skulle få av att spela en omgång Swell Foop (version 2.8.0) och sedan avsluta. Postscript-filen kommer ha ett namn som massif.12345.ps och textfilen kommer att kallas massif.12345.txt. Numret i mitten är process-ID:t för programmet som undersöktes. Om du faktiskt prövar detta exempel kommer du hitta två versioner av varje fil, med något olika nummer, detta beror på att Swell Foop startar en andra process och Massif följer den också. Vi kommer ignorera denna andra process, den förbrukar mycket lite minne.

Högst upp på grafen ser vi ett stort gult band märkt gdk_pixbuf_new. Detta verkar vara en ideal kandidat för optimering, men vi kommer behöva använda textfilen för att få reda på vad som anropar gdk_pixbuf_new. Början på textfilen kommer se ut något i stil med detta:

Command: ./swell-foop

== 0 =========================== Heap allocation functions accounted for 90.4% of measured spacetime

Called from: 28.8% : 0x6BF83A: gdk_pixbuf_new (in /usr/lib/libgdk_pixbuf-2.0.so.0.400.9)

6.1% : 0x5A32A5: g_strdup (in /usr/lib/libglib-2.0.so.0.400.6)

5.9% : 0x510B3C: (within /usr/lib/libfreetype.so.6.3.7)

3.5% : 0x2A4A6B: __gconv_open (in /lib/tls/libc-2.3.3.so)

Raden med ”=”-tecknen indikerar hur långt ner för stackspåret vi är, i detta fall på toppen. Efter detta listar det de tyngsta användarna av minne ordnade efter sjunkande utrymmestid. Utrymmestid är produkten av mängden minne som används och hur länge det användes. Det motsvarar arean av banden i grafen, Denna del av filen talar om för oss vad vi redan vet: att den mesta utrymmestiden ägnas åt gdk_pixbuf_new. För att ta reda på vad som anropade gdk_pixbuf_new måste vi söka längre ner i textfilen:

== 4 =========================== Context accounted for 28.8% of measured spacetime 0x6BF83A: gdk_pixbuf_new (in /usr/lib/libgdk_pixbuf-2.0.so.0.400.9) 0x3A998998: (within /usr/lib/gtk-2.0/2.4.0/loaders/libpixbufloader-png.so) 0x6C2760: (within /usr/lib/libgdk_pixbuf-2.0.so.0.400.9) 0x6C285E: gdk_pixbuf_new_from_file (in /usr/lib/libgdk_pixbuf-2.0.so.0.400.9)

Called from: 27.8% : 0x804C1A3: load_scenario (swell-foop.c:463)

0.9% : 0x3E8095E: (within /usr/lib/libgnomeui-2.so.0.792.0)

and 1 other insignificant place

Den första raden säger oss att vi är fyra nivåer ner i stacken. Under den är en lista över de funktionsanrop som leder härifrån till gdk_pixbuf_new. Slutligen finns det en lista över funktioner som är på nästa nivå neråt och anropar dessa funktioner. Det finns förstås även poster för nivå 1, 2 och 3, men detta är den första nivån som når rakt genom GDK-koden till Swell Foop-koden. Från denna lista kan vi omedelbart se att problemkoden är load_scenario.

Nu när vi vet vilken del av vår kod som använder all utrymmestid kan vi titta på den och ta reda på varför. Det visar sig att load_scenario läser in en pixbuf från fil och sedan aldrig frigör det minnet. Eftersom vi har identifierat problemkoden så kan vi fixa den.

# Agera på resultaten

Att reducera användning av utrymmestid är bra, men det finns två sätt att minska det och de är inte likvärdiga. Du kan antingen minska mängden minne som allokeras, eller minska tidsperioden som det är allokerat. Tänk dig nu ett modalt system där bara två processer körs. Båda processerna använder upp nästan allt fysiskt RAM-minne, och om de någonsin överlappar kommer systemet använda växlingsutrymme och allt kommer att sakta ner. Uppenbarligen kan de samexistera utan behov att använda växlingsutrymme om vi minskar minnesanvändningen för varje process med en faktor två. Om vi istället minskar tiden minne är allokerat kan programmen samexistera, men bara så länge deras perioder av hög minnesanvändning inte överlappar. Det är därför bättre att minska mängden allokerat minne.

Dessvärre begränsas valet av optimering även av programmets behov. Storleken på pixbuf-data i Swell Foop avgörs av storleken på spelets grafik och kan inte minskas på något lätt sätt. Tidsperioden som det är inläst i minne kan dock minskas drastiskt. Bilden nedan visar analysen Massif har gjort av Swell Foop efter att programmet ändrats för att göra sig av med pixbufar då bilderna har lästs in i X-servern.

## Massif-utdata för det optimerade Swell Foop-programmet.

Användningen av utrymmestid för gdk_pixbuf_new är nu ett tunt band som endast tillfälligt når toppar (det är nu det sextonde bandet uppifrån och har färgen magenta). Som en bonus minskar minnesanvändningstoppen med 200 kB efter det att toppen inträffat innan annat minne allokeras. Om två processer som denna körs samtidigt är risken att topparna inträffar samtidigt, och därmed risken att växlingsutrymme behöver användas, ganska låg.

Kan vi göra bättre? En snabb undersökning av textutdata från Massif visar att g_strdup nu är den största minnestjuven.

Command: ./swell-foop

== 0 =========================== Heap allocation functions accounted for 87.6% of measured spacetime

Called from: 7.7% : 0x5A32A5: g_strdup (in /usr/lib/libglib-2.0.so.0.400.6)

7.6% : 0x43BC9F: (within /usr/lib/libgdk-x11-2.0.so.0.400.9)

6.9% : 0x510B3C: (within /usr/lib/libfreetype.so.6.3.7)

5.2% : 0x2A4A6B: __gconv_open (in /lib/tls/libc-2.3.3.so)

Om vi tittar närmare ser vi dock att det anropas från många, många ställen.

== 1 =========================== Context accounted for 7.7% of measured spacetime 0x5A32A5: g_strdup (in /usr/lib/libglib-2.0.so.0.400.6)

Called from: 1.8% : 0x8BF606: gtk_icon_source_copy (in /usr/lib/libgtk-x11-2.0.so.0.400.9)

1.1% : 0x67AF6B: g_param_spec_internal (in /usr/lib/libgobject-2.0.so.0.400.6)

0.9% : 0x91FCFC: (within /usr/lib/libgtk-x11-2.0.so.0.400.9)

0.8% : 0x57EEBF: g_quark_from_string (in /usr/lib/libglib-2.0.so.0.400.6)

and 155 other insignificant places

Vi möter nu avtagande resultat för våra optimeringsförsök. Grafen tipsar om en annan möjlig infallsvinkel: både bandet ”other” och ”heap admin” är ganska stort. Detta säger oss att det finns många små allokeringar som görs från olika ställen. Att eliminera dessa blir svårt, men om de kunde grupperas skulle individuella allokeringar kunna vara större och kostnaden för ”heap admin” kunna minskas.

# Förbehåll

Det finns några saker att vara uppmärksam på: den första är att utrymmestid endast rapporteras som en procentsats, du måste jämföra den med programmets totala storlek för att avgöra om mängden minne är värd titta närmare på. Grafen är med sin vertikala kilobyteaxel bra för detta.

Den andra saken är att Massif endast räknar in minne använt av ditt eget program. Resurser som pixmappar lagras i X-servern och räknas inte av Massif. I Swell Foop-exemplet har vi faktiskt bara förflyttat minnesförbrukningen från pixbufar på klientsidan till pixmappar på serversidan. Även om vi fuskade uppstår det prestandavinster. Att behålla bilddata i X-servern gör grafikrutinerna snabbare och tar bort mycket kommunikation mellan processer. Pixmapparna kommer också lagras i ett inhemskt grafikformat vilket ofta är mer kompakt än det 32-bitars RGBA-format som används av gdk_pixbuf. För att mäta effekten av pixmappar och andra X-resurser kan du använda programmet xrestop.