Speicheregionen wurden in der voreingestellten C++ Speicherzuordnung unter Linux eingeführt, um die Leistungsfähigkeit speicherintensiver Multithread-Anwendungen zu verbessern. Zuvor musste jede Speicherzuordnung synchronisiert werden. Dies führte dazu, dass die Speicherzuordnung häufig Leistungsengpässe verursachte.
Speicherregionen lösen dieses Problem durch die Einrichtung mehrerer Speicher-Pools. Diese unterstützen die Speicherzuordnung mit mehreren simultanen Threads (mehr über Speicherregionen erfahren Sie in diesem hervorragenden Blogbeitrag: https://sploitfun.wordpress.com/2015/02/10/understanding-glibc-malloc/).
In dem Blogbeitrag geht es um die Frage, wie sich dies auf das beobachtete Speicherverhalten auswirkt. Diese Frage ist deshalb relevant, weil es fälschlicherweise leicht als Speicherleak interpretiert werden kann.
Das Programm startet mehrere separate Workerthreads. Dabei gibt es zwei verschiedene Arten von Threads: 30MB-Workerthreads und 100MB-Workerthreads. Der 30MB-Workerthread weist 30 MB Daten zu und initialisiert sie.
Die Daten werden nach 100 Millisekunden freigegeben. Das passiert zehnmal nacheinander. Der 100MB-Workerthread tut genau dasselbe, allerdings mit 100MB-Blöcken. Welche Art von Workerthread und wie viele davon ausgelöst werden, lässt sich über Befehlszeilenparameter einstellen.
Sobald alle Workerthreads ihre Arbeit getan haben, wartet das Programm auf weitere Benutzerbefehle. Der Benutzer hat drei Möglichkeiten: Die erste beendet das Programm, die zweite führt malloc_trim(0) aus und die dritte druckt über malloc_stats() Statistiken zur Speicherregion aus.
Die folgenden Versuche wurden unter Ubuntu 16.04 mit einer 8-Kern CPU (4 physische) durchgeführt.
Die Tabelle unten veranschaulicht den residenten Prozessspeicher für unterschiedliche Worker-Typen und -Anzahlen, nachdem alle Worker ihre Arbeit erledigt und sämtliche Daten freigegeben haben.
Die 900 MB residenter Speicher nach dem vierten Durchlauf lohnen einen genaueren Blick. Die naheliegende Vermutung wäre ein Speicherleck, das sich aber mithilfe von Tools wie Valgrind ausschließen lässt. Die eigentliche Ursache finden wir, wenn wir einen Blick auf die Statistiken zu den Speicherregionen werfen, die wir über malloc_stats() erhalten. Beim ersten Durchlauf erzeugt malloc_stats() beispielsweise die folgende Ausgabe:
Eine Erläuterung dieses Konzepts finden Sie in diesem Artikel, der auf einem „Lightning Talk“ basiert, welcher anlässlich einer Konferenz der C++ Anwendergruppe in München gehalten wurde und beschreibt, wie Speicherregionen zu vermeintlichen Speicherlecks führen können. In beiden Fällen wird das folgende Programm verwendet: https://github.com/celonis-se/memory-arena-example/blob/master/main.cpp
Die Ausgabedaten zeigen, wie viele Speicherregionen vorhanden sind, wie groß diese sind und wie viel Speicher tatsächlich genutzt wird. Die Ergebnisse der einzelnen Testläufe finden sich in der folgenden Tabelle:
Die Tests deuten darauf hin, dass für jeden Workerthread eine eigenständige Speicherregion erstellt wird. Außerdem scheint es so, als machten die Speicherregionen keinen Speicherplatz für das Betriebssystem verfügbar, obwohl dieses vollkommen leer ist.
Die Unterschiede in der Größe der Speicherregionen zwischen den 100MB-Workerthreads und den 30MB-Workerthreads lässt sich dadurch erklären, dass große zugewiesene Speicherblöcke anders als kleine behandelt werden. Denn während kleinere Speicherblöcke einer bestimmten Speicherregion zugeordnet werden, erfolgt die Zuordnung größerer Blöcke per mmap, um eine übermäßige Fragmentierung zu vermeiden. Weitere Informationen zu diesem Thema finden Sie hier: https://www.linuxjournal.com/article/6390
Im Hinblick auf das Problem der fehlenden Rückgabe des Speicherplatzes an das Betriebssystem empfiehlt sich die Lektüre eines Posts auf stackoverflow zum Thema Speichermanagement in Multi-Thread-Umgebungen mit malloc_trim.
Die folgende Tabelle zeigt die Statistiken zu den Speicherregionen nach der Durchführung von malloc_trim. Laut Dokumentation versucht dieses, von oben freien Speicherplatz aus dem Heap freizugeben:
Die Tests zeigen, dass nur 30 MB – die Größe einer Speicherregion – freigegeben werden. Weitere Untersuchungen zeigen, dass malloc_trim ausschließlich die ungenutzten Bytes aus dem Hauptspeicher an das Betriebssystem zurückgibt. Nach unserem Kenntnisstand existiert derzeit keine Möglichkeit, den Speicherplatz für das Betriebssystem nutzbar zu machen, was wiederum einen beträchtlichen Speicher-Overhead verursachen kann.
Um diesen zu begrenzen, ist die Anzahl der Speicherregionen standardmäßig auf das Achtfache der Anzahl an Cores begrenzt (einschl. Hyperthreads). Da das hier verwendete Gerät acht Kerne besitzt, sind somit 64 Speicherregionen möglich.
Wenn mehr als 64 Workerthreads aktiv sind, werden keine weiteren Speicherregionen erstellt. Stattdessen können die vorhandenen aber wachsen.
Speicherregionen können die Leistungsfähigkeit deutlich verbessern, aber auch einen beträchtlichen Speicher-Overhead verursachen. Das gilt insbesondere für Multi-Threaded-Programme mit langer Laufzeit.
Dieses Verhalten kann leicht als Speicherleck missverstanden werden. Bestehen Zweifel, ob tatsächlich ein Speicherleck vorliegt oder der Speicher in Speicherregionen aufgeteilt worden ist, kann dies über malloc_stats() mithilfe der Statistiken überprüft werden.