Mario Blättermann
mario.blaettermann@gmail.com 2009, 2012, 2013, 2015
Christian Kirbach
christian.kirbach@googlemail.com 2010

# Verwendung von Massif zur Profilierung des Speicherverbrauchs in GNOME-Software

Dieser Artikel beschreibt, wie Sie den Heap-Profiler Massif mit GNOME-Anwendungen verwenden. Wir beschreiben den Aufruf, die Interpretation und die Möglichkeiten, die Ausgaben von Massif richtig zu nutzen. Das Spiel Swell Foop wird als Beispiel verwendet.

# Einführung

Massif ist Teil der Programmsammlung valgrind, die Werkzeuge zur Speicherprofilierung bereitstellt. Zweck ist die Erstellung eines detaillierten Überblicks über die dynamische Speichernutzung während der Lebensdauer eines Programms. Speziell wird der Speicherverbrauch von »heap« und »stack« aufgezeichnet.

Der Heap ist der Speicherbereich, der von Funktionen wie »malloc« belegt wird. Er wächst bei Bedarf und ist gewöhnlich der größte Speicherbereich eines Programms. Der Stapelspeicher (Stack) dient zur Speicherung der lokalen Daten von Funktionen. Dies schließt »automatische« Variablen in C und die Rückgabeadresse von Subroutinen ein. Der Stapelspeicher ist typischerweise wesentlich kleiner und deutlich aktiver als der Heap. Wir werden den Stapelspeicher nicht gesondert betrachten, weil ihn Massif wie einen weiteren Teil des Heap ansieht. Massif liefert auch Informationen darüber, wie viel Arbeitsspeicher für den Heap belegt wird.

Massif erstellt zwei Ausgabdateien: eine grafische Übersicht in einer PostScript-Datei und eine detaillierte Ausgabe in einer Textdatei.

# Massif mit GNOME verwenden

Massif hat sehr wenige Optionen und für viele Programme brauchen Sie diese auch gar nicht. Für GNOME-Anwendungen jedoch muss die Anzahl der Abstiegsebenen des Aufruf-Stapelspeichers erhöht werden, weil sie bei diesen Speicheranforderungen tief in glib oder GTK+ verborgen liegen. Dies wird mit dem Parameter »--depth« erreicht. Die Voreinstellung ist 3; eine Erhöhung auf 5 stellt sicher, dass der Aufruf-Stapelspeicher bis zu Ihrem Code reicht. Ein oder zwei weitere Ebenen können auch wünschenswert sein, um weiteren Kontext zu ersehen. Weil die Detailangaben sehr schnell erdrückend werden können ist es am besten, mit dem kleineren Parameter zu beginnen und ihn nur bei Bedarf zu erhöhen.

Es ist auch nützlich, Massif mitzuteilen, welche Funktionen in glib Arbeitsspeicher anfordern. So wird eine unnötige Ebene Funktionsaufrufe aus den Berichten entfernt und verbessert den Einblick, welcher Code Speicher anfordert. Speicher anfordernde Funktionen in glib sind: g_malloc, g_malloc0, g_realloc, g_try_malloc und g_mem_chunk_alloc. Verwenden Sie die Option »--alloc-fn«, um Massif diese Namen mitzuteilen.

Ihre Befehlszeile sollte daher so aussehen:

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

Das Programm Swell Foop werden wir als Beispiel verwenden. Seien Sie gewarnt: Valgrind emuliert den Prozessor, wodurch es äußerst langsam läuft. Außerdem wird sehr viel Speicher benötigt.

# Interpretieren der Ergebnisse

Die grafische Ausgabe von Massif ist weitgehend selbsterklärend. Jedes Band stellt den einer Funktion zugewiesenen Speicher nach Zeit dar. Sobald Sie herausgefunden haben, welche Bänder den meisten Speicher verbrauchen, üblicherweise die dickeren, oberen, dann schauen Sie sich die Textausgabe für weitere Details an.

Die Textdatei ist hierarchisch in Abschnitte gegliedert. Am oberen Ende finden sich die größten Speicherverbraucher, in absteigender Ordnung nach Speicherzeit. Darunter finden sich weitere Abschnitte, die je nach dem Platz im Aufruf-Stack feiner detaillierter sind. Um dies zu verdeutlichen, verwenden wir die Ausgabe des oben stehenden Befehls.

## Massif-Ausgabe für die nicht optimierte Version des Programms Swell Foop.

Das Bild oben zeigt eine typische PostScript-Ausgabe von Massif. Dies ist das Ergebnis, wenn Sie eine einzige Runde Swell Foop (Version 2.8.0) spielen und dann die Anwendung schließen. Die PostScript-Datei hat einen Namen ähnlich wie massif.12345.ps und die Text-Datei massif.12345.txt. Die Zahl im Dateinamen entspricht der Prozesskennung des untersuchten Programms. Wenn Sie dieses Beispiel nachstellen, so werden Sie zwei Versionen jeder Datei mit leicht verschiedenen Nummern finden. Dies liegt daran, dass Swell Foop einen zweiten Prozess startet und Massif diesen ebenfalls protokolliert. Wie werden diesen zweiten Prozess ignorieren. Er verbraucht nur sehr wenig Speicher.

Oben in der Grafik sehen Sie ein großes gelbes Band mit der Bezeichnung gdk_pixbuf_new. Dies schaut nach einem idealen Kandidaten für Optimierungsversuche aus, aber wir werden die Textdatei verwenden müssen, um herauszufinden, wo gdk_pixbuf_new aufgerufen wird. Der Anfang der Textdatei sieht in etwa so aus:

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)

Die Zeile mit »=«-Zeichen deutet an, wie tief wir uns in der Stack-Trace befinden. In diesem Fall sind wir ganz oben. Danach werden die größten Speicherbeleger nach absteigender Raumzeit aufgeführt. Raumzeit ist das Produkt der Menge des belegten Speichers und wie lange er belegt wurde. Dies entspricht der Fläche der Bänder im Grafen. Dieser Teil der Datei belegt, was wir bereits wissen: der Großteil der Raumzeit ist gdk_pixbuf_new gewidmet. Um herauszufinden, was gdk_pixbuf_new aufrief, müssen wir weiter unten in der Textdatei suchen:

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

Die erste Zeile besagt, dass wir uns nun vier Ebenen tief im Stapelspeicher befinden. Darunter ist eine Auflistung der Funktionsaufrufe, die von hier bis gdk_pixbuf_new reichen. Schließlich findet sich eine Liste von Funktionen, die sich eine Ebene darunter befinden und jene Funktionen aufrufen. es gibt natürlich auch Einträge für die Ebenen 1 bis 3, aber dies ist die erste Ebene, die man über den GDK-Code bis zum Swell Foop-Code erreichen kann. Aus der Auflistung kann man sofort erkennen, dass der problematische Code in load_scenario ist.

Jetzt, wo wir wissen, welcher Teil unseres Codes alle Raumzeit belegt, können wir ihn betrachten und die Ursache herausfinden. Es stellt sich heraus, dass load_scenario einen Pixbuf aus einer Datei lädt und diesen Speicher nie mehr frei gibt. Nachdem das Problem identifiziert wurde, können wir es beheben.

# Aufgrund der Ergebnisse handeln

Ein Reduzieren des Verbrauches an Raumzeit ist gut, aber es gibt zwei verschiedene Wege, dies zu tun. Sie können entweder die angeforderte Speichermenge oder die Belegungszeit reduzieren. Betrachten Sie für einen Moment ein Modellsystem mit nur zwei laufenden Prozessen. Beide Prozesse belegen fast allen physikalischen Arbeitsspeicher und sobald sie sich überlappen, wird das System auslagern müssen und es wird sehr langsam. Es ist offensichtlich, dass die beiden Prozesse friedlich nebeneinander existieren können, wenn wir den Speicherverbrauch jedes Prozesses halbieren. Wenn wir statt dessen die Speicherbelegungszeit halbieren, dann können die beiden Prozesse nebeneinander existieren, aber nur solange ihre Zeiträume hoher Speicherbelegung nicht überlappen. Demnach ist es besser, die Speicherbelegung zu reduzieren.

Unglücklicherweise schränken die Erfordernisse des Programms die Wahl der Optimierung ein. Die Größe der Pixbuf-Daten in Swell Foop ist von der Größe der Grafiken im Spiel abhängig und kann nicht ohne Weiteres reduziert werden. Allerdings kann die Zeit zum Laden des Programms in den Speicher drastisch reduziert werden! Das Bild unten zeigt die Massif-Analyse von Swell Foop nach den Änderungen, die die Pixbufs entsorgen, sobald diese vom X-Server geladen wurden.

## Massif-Ausgaben für das optimierte Programm Swell Foop.

Die Raumzeit-Belegung von gdk_pixbuf_new ist nun nur noch ein dünnes Band, das nur kurzzeitig einen Spitzenwert hat (es ist nun das sechzehnte schraffierte magenta Band von oben). Als Bonus ist der Spitzenspeicherverbrauch um 200 kB gesunken, weil die Spitze vor anderen Speicheranforderungen liegt. Wenn zwei solche Prozesse zeitgleich liefen, wäre die Wahrscheinlichkeit ziemlich gering, dass der höchste Speicherverbrauch beider zusammen fällt.

Können wir noch zulegen? Eine schnelle Untersuchung der Textausgabe von Massif enthüllt: g_strdup ist der neue Hauptschuldige.

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)

Bei näherer Betrachtung sehen wir jedoch, dass es von vielen verschiedenen Orten aus aufgerufen wird.

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

Nun begegnen wir geschmälerten Resultaten für unsere Bemühungen. Der Graph deutet einen weiteren möglichen Schritt an: Die Bänder »other« und »heap admin« sind beide ziemlich groß. Dies besagt, dass eine Vielzahl kleiner Anforderungen an vielen Orten gemacht werden. Jene zu eliminieren ist schwierig, aber wenn sie zusammen gelegt werden, dann werden die einzelnen Anforderungen größer und der Verwaltungsaufwand (»heap admin«) kann reduziert werden.

# Vorbehalte

Es gibt ein paar Dinge, auf die man achten muss. Erstens: Raumzeit wird nur als Prozentangabe geliefert. Sie müssen mit der Gesamtgröße des Programms vergleichen, um festzustellen, ob es sich lohnt, diese Speichermenge anzugehen. Der Graph mit der vertikalen Achse in Kilobytes eignet sich gut dafür.

Zweitens: Massif erfasst nur die Speicherbelegung durch Ihr eigenes Programm. Ressourcen wie Pixmaps werden im X-Server gespeichert und werden nicht durch Massif berücksichtigt. In dem Beispiel mit Swell Foop haben wir tatsächlich nur den Speicherverbrauch von client-seitigen Pixbufs zu server-seitigen Pixmaps verlagert. Obwohl dies nur nach Schummeln aussieht, gab es dennoch Geschwindigkeitsgewinne. Bilddaten im X-Server beschleunigen die Grafik-Routinen und verhindert viele Inter-Prozess-Kommunikation. Ebenso werden die Pixmaps in nativem Grafikformat gespeichert, welches oft kompakter ist als das 32-bit RGBA-Format von gdk_pixbuf. Um den Effekt von Pixmaps und anderen Ressourcen des X-Servers zu messen, sollten Sie das Programm xrestop verwenden.