Packstation

Läuft ein Programm langsamer, als es sollte, ist meist nicht der Compiler schuld. Oft findet sich die Ursache in der Anwendung selbst, etwa in der internen Speicherverwaltung.

vorlesen Druckansicht 3 Kommentare lesen
Lesezeit: 4 Min.
Von
  • Michael Riepe

Beim Entwickeln einer neuen Anwendung kommt es in erster Linie darauf an, dass sie korrekt arbeitet. Geschwindigkeit ist zweitrangig – der optimierende Compiler wird’s schon richten; außerdem werden Rechner ohnehin jedes Jahr schneller. Für weitergehende Optimierungen fehlt oft die Zeit: Kommerzielle Anwendungen müssen auf den Markt, wissenschaftliche sollen Ergebnisse liefern.

Moderne Software verwendet in der Regel dynamisch allozierten Speicher. Läuft ein Programm trotz geballter Rechenleistung nicht schnell genug, kann es sich daher lohnen, einen Blick auf seine Speicherverwaltung zu werfen. Die Benchmarks aus der CPU2006-Suite der SPEC (siehe Kasten „Onlinequellen“) etwa, die überwiegend auf wissenschaftlichen Anwendungen beruhen, liefern zum Teil deutlich bessere Ergebnisse, wenn man beim Übersetzen die kommerzielle SmartHeap-Bibliothek von MicroQuill einbindet. Sie ersetzt die Funktionen zum Anfordern von Speicher durch eigene Routinen, die in vielen Fällen effizienter arbeiten.

Unter anderem erreichen sie das, indem sie unnötige Systemaufrufe vermeiden. Ein Aufruf von malloc oder new ist vergleichsweise kostspielig, wenn die Laufzeitbibliothek die gewünschte Speichermenge erst beim Betriebssystem anfordern muss. Kommt das millionenfach vor, was bei größeren Anwendungen durchaus keine Seltenheit ist, können sich die Wartezeiten zu Sekunden oder gar Minuten summieren. Wie oft ein Programm Speicher anfordert, lässt sich unter Linux/Unix zum Beispiel mit strace feststellen: Viele Aufrufe von brk, sbrk oder mmap deuten darauf hin, dass die Speicherverwaltung ineffizient arbeitet.

Zwar führen viele Laufzeitbibliotheken kleinere Optimierungen durch, sodass nicht jeder Anruf bei malloc zu einem teuren Ferngespräch mutiert. Doch dabei handelt es sich stets um einen Kompromiss zwischen Laufzeitverhalten und niedrigem Speicherbedarf – traditionell zugunsten des Letzteren –, der nicht allen Bedürfnissen gerecht werden kann. Weiß der Programmierer jedoch, dass seine Software einige Hundert Megabyte RAM in kleinen Stücken verlangt, kann er entsprechend vorsorgen.

Mehr Infos

Steht der Gesamtbedarf einer Anwendung von vornherein fest, empfiehlt es sich unter Umständen, den benötigten Speicher in alter Fortran-Manier statisch zu allozieren – ohne ihn zu initialisieren, da er sonst Speicherplatz in der Programmdatei belegt – und ihn während des Programmlaufs häppchenweise zuzuteilen. Variiert der Speicherbedarf, bietet es sich an, Speicher in größeren Blöcken zu allozieren und diese zu tranchieren. Jeder Block sollte Platz für viele – einige Hundert oder Tausend – Einzelobjekte bieten, sodass sich die Zahl der Systemaufrufe drastisch reduziert. Große Objekte, die nicht in einen Block passen, sowie die Speicherblöcke selbst kann man mit herkömmlichen Mitteln wie malloc oder mmap anlegen.

Ein solcher Speicher-Pool lässt sich einfacher verwalten, wenn man für Objekte unterschiedlicher Größe separate Speicherblöcke verwendet, die in gleich große Portionen unterteilt sind. Die Allozierungsroutine muss dann lediglich den passenden Speicherblock finden und darin den ersten ungenutzten Abschnitt aufspüren – was gewöhnlich auch der Performance zugutekommt. Noch nicht oder nicht mehr genutzte Objekte lassen sich in Form einer einfach verketteten Liste darstellen. Der Aufwand für das Allozieren und Freigeben von Speicher beschränkt sich dadurch auf wenige, einfache Operationen, die kaum Zeit in Anspruch nehmen.

Wer eine fertige Lösung sucht, kann zum Beispiel auf die Bibliothek libmempool zurückgreifen. Sie steht allerdings unter Version 3 der GNU General Public License (GPL), was zwangsläufig auf alle Anwendungen abfärbt, die sie nutzen – sofern man die Absicht hat, diese zu veröffentlichen.

Die Programmierschnittstelle von libmempool ist der der C-Laufzeitbibliothek nachempfunden; die Funktionen mempool_malloc, mempool_calloc, mempool_realloc, mempool_strdup und mempool_free verlangen lediglich als zusätzliches erstes Argument einen Zeiger auf einen Memory Pool. Den kann man mit create_mempool erzeugen und nach Gebrauch mit destroy_mempool wieder entsorgen. Speicher fordert die Bibliothek bei Bedarf vom Betriebssystem an, sofern man ihn nicht mit mempool_preallocate vorab alloziert.

Ein zusätzliches Highlight ist die Option, nach Speicherlecks in der Anwendung zu suchen. Setzt der Entwickler die Variable mempool_debug auf einen Wert ungleich null, gibt destroy_mempool eine Liste aller nicht freigegebenen Speicherbereiche aus. Verwendet er außerdem die Debugging-Versionen der Bibliotheksfunktionen, etwa mempool_malloc_debug statt mempool_malloc, zeigt die Bibliothek obendrein an, an welcher Stelle des Programms der Speicher angefordert wurde.

iX-Link (mr)