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

# Using Massif for Profiling Memory Use in GNOME Software

This article describes how to use the Massif heap profiler with GNOME applications. We describe how to invoke, interpret, and act on the output of Massif. The Swell Foop game is used as an example.

# Einführung

Massif is a member of the valgrind suite of memory-profiling tools. Its purpose is to give a detailed view of dynamic memory usage during the lifetime of the program. Specifically it records the memory use of the heap and the stack.

The heap is the region of memory which is allocated with functions like malloc. It grows on demand and is usually the largest region of memory in a program. The stack is where all the local data for functions is stored. This includes the "automatic" variables in C and the return address for subroutines. The stack is typically a lot smaller and a lot more active than the heap. We won't consider the stack explicitly since Massif treats it as though it were just another part of the heap. Massif also gives information about how much memory is used to manage the heap.

Massif produces two output files: a graphical overview in a postscript file and a detailed breakdown in a text file.

# Using Massif with GNOME

Massif has very few options and for many programs does not need them. However for GNOME applications, where memory allocation might be buried deep in either glib or GTK, the number of levels down the call-stack Massif descends needs to be increased. This is achieved using the --depth parameter. By default this is 3; increasing it to 5 will guarantee the call-stack reaches down to your code. One or two more levels may also be desirable to provide your code with some context. Since the level of detail becomes quickly overwhelming it is best to start with the smaller depth parameter and only increase it when it becomes apparent that it isn't sufficient.

It is also useful to tell Massif which functions allocate memory in glib. It removes an unnecessary layer of function calls from the reports and gives you a clearer idea of what code is allocating memory. The allocating functions in glib are g_malloc, g_malloc0, g_realloc, g_try_malloc, and g_mem_chunk_alloc. You use the --alloc-fn option to tell Massif about them.

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

Swell Foop is the program we will be using as an example. Be warned that, since valgrind emulates the CPU, it will run very slowly. You will also need a lot of memory.

# Interpretieren der Ergebnisse

The graphical output of Massif is largely self explanatory. Each band represents the memory allocated by one function over time. Once you identify which bands are using the most memory, usually the big thick ones at the top you will have to consult the text file for the details.

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 output for the unoptimized version of the Swell Foop program.

shows a typical postscript output from Massif. This is the result you would get from playing a single game of Swell Foop (version 2.8.0) and then quitting. The postscript file will have a name like massif.12345.ps and the text file will be called massif.12345.txt. The number in the middle is the process ID of the program that was examined. If you actually try this example you will find two versions of each file, with slightly different numbers, this is because Swell Foop starts a second process and Massif follows that too. We will ignore this second process, it consumes very little memory.

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

The first line tells us we are now four levels deep into the stack. Below it is a listing of the function calls that leads from here to gdk_pixbuf_new. Finally there is a list of functions that are at the next level down and call these functions. There are, of course, also entries for levels 1, 2, and 3, but this is the first level to reach right down through the GDK code to the Swell Foop code. From this listing, we can see instantly that the problem code is load_scenario.

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.

Unfortunately, the choice of optimization is also constrained by the needs of the program. The size of the pixbuf data in Swell Foop is determined by the size of the game's graphics and cannot be easily reduced. However, the amount of time it spends loaded into memory can be drastically reduced. shows the Massif analysis of Swell Foop after being altered to dispose of the pixbufs once the images have been loaded into the X server.

## Massif output for the optimized Swell Foop program.

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.

Can we do better ? A quick examination of Massif's text output reveals: g_strdup to be the new major offender.

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.

Secondly, Massif only takes into account the memory used by your own program. Resources like pixmaps are stored in the X server and aren't considered by Massif. In the Swell Foop example we have actually only moved the memory consumption from client-side pixbufs to server-side pixmaps. Even though we cheated there are performance gains. Keeping the image data in the X server makes the graphics routines quicker and removes a lot of inter-process communication. Also, the pixmaps will be stored in a native graphics format which is often more compact than the 32-bit RGBA format used by gdk_pixbuf. To measure the effect of pixmaps, and other X resources use the xrestop program.