Early first draft Höllische Programmiersprachen Seminar im WS 2014/15 Speichermanagement Max Haslbeck Technische Universität München 20.01.2015 Zusammenfassung 1 Einleitung 2 Begriffsklärung Heutzutage erlauben fast alle modernen Programmiersprachen Bereiche des Speichers (Details der Hardwareimplementierung des Speichers sind im Rahmen dieser Arbeit uninteressant) dynamisch anzufordern, d.h. Speicherbereiche zu belegen deren Größe zur Kompilierzeit noch nicht bekannt ist und die auch länger existieren können als die Funktion (oder Subroutine, Methode, ) die sie erstellt hat. Diese Objekte werden auf dem Heap abgelegt, im Gegensatz zum Stack der jeweiligen Funktion. Dynamisch Speicher zu belegen ermöglicht Programmierern verschiedene Dinge wie Objekte verschiedener Größe zu erstellen oder rekursive Datenstrukturen wie Listen oder Bäume zu benutzen. Auf Objekte im Heap kann durch Referenzen, oder auch Pointer genannt, zugegriffen werden. Wie diese genau aussehen hängt von der jeweiligen Implementierung ab. Oft handelt es sich um nichts anderes als die Adressen der jeweiligen Speicherblöcke. 3 Manuelles Speichermanagement C/C++ sind praktisch die einzigen Programmiersprachen, die noch direktes manuelles Speichermanagement erlauben. Die Befehle malloc() und free () werden zum reservieren und zum freigeben von Speicherblöcken verwendet. malloc() nimmt als Argument die Größe des benötigten Speicherplatzes, versucht diesen 1
zu reservieren und gibt dann einen Pointer auf diesen Speicherplatz zurück. Falls dies nicht funktioniert, da z.b. der Heap voll ist, wird ein NULL-Pointer oder eine speziell reservierte Adresse zurückgegeben, je nach Implementierung [1]. Die einfachste Methode Speichermanagement zu betreiben ist natürlich gar kein Speichermanagement zu betreiben. Da das Betriebssystem den von einem Programm angeforderten Speicher nach dessen Beendigung komplett wieder freigibt, ist das durchaus eine Möglichkeit für Programme die nur kurz laufen. Dies kann auch funktionieren, wenn man sich der genauen Speicheranforderungen des Programms bewusst ist und diese definitiv den Heap nicht komplett aufbrauchen. 2 Dies ist natürlich keine Option für Programme mit hohen Speicheranforderungen oder mit hohen Laufzeiten wie z.b. UNIX-daemons ist. Hier liegt es in der Hand des Programmierers nicht mehr benötigten Speicher wieder freizugeben und Referenzen auf undefinierte Speicherbereiche zu vermeiden. Die Komplexität dieses Problems geht natürlich Hand in Hand mit der Komplexität eines Programms. Es gibt mittlerweile auch viele Tools die Programmieren helfen Fehler, wie die unten genannten, zu vermeiden, z.b. valgrind. 3.1 Memory leaks Memory leaks oder zu deutsch Speicherlecks sind einer der bekanntesten Fehler, die bei manueller Speicherverwaltung auftreten können. Hier ein Beispiel in C: #i n c l u d e <s t d l i b. c> void l e a k ( ) 4 { 6 i n t a = malloc ( s i z e o f ( i n t ) ) ; // S p e i c h e r p l a t z wird // r e s e r v i e r t 8 // Von a b e l e g t e r S p e i c h e r wird n i c h t f r e i g e g e b e n 10 } 12 i n t main ( ) { 14 l e a k ( ) 16 // dann z.b. w i e d e r h o l t e s Aufrufen von l e a k ( ) } Bei jedem Aufruf von leak() wird Speicherplatz reserviert, aber nicht mehr freigegeben. Außerhalb der Funktion leak() gibt es auch keine Möglichkeit mehr diesen 2
2 Speicherplatz zu referenzieren. Es kommt zum sogenannten memory leak. Wiederholtes Aufrufen von leak() (z.b. in einer Dauerschleife) führt zu zunehmenden Verschwendung von Speicherplatz auf dem Heap. Was dann normalerweise zur Beendigung des Programms durch das Betriebssystem führt, wenn kein Platz mehr auf dem Heap vorhanden ist. 3.2 Dangling pointers Bei dangling pointers handelt es sich um Referenzen auf Speicherbereiche, die nicht den erwarteteten Wert enthalten. Das kann passieren, wenn Speicherbereiche nicht installiert wurden oder freigegeben wurden und auf sie zugegriffen wird. Ein Beispiel in C: #i n c l u d e <s t d l i b. c> void dang ( i n t p o i n t e r ) 4 { 6 i n t a = p o i n t e r ; 8 f r e e a ; } 10 i n t main ( ) 12 { i n t p = malloc ( s i z e o f ( i n t ) ) ; 14 dang ( p ) ; 16 // Z u g r i f f auf den von p r e f e r e n z i e r t e n S p e i c h e r b e r e i c h // f u e h r t j e t z t zu u n d e f i n i e r t e m Verhalten 18 } 4 Automatisches Speichermanagement Um den Speicher automatisch zu verwalten gibt es mehrere Methoden und vieles hat sich getan seit John McCarthy in LISP zum ersten Mal einen garbage collector zur Speicherverwaltung benutzte [6]. Im Folgenden wird nur auf die grundlegenden Algorithmen eingegangen. Heute Speichermanagementsysteme beruhen auf diesen, wenn auch natürlich in optimierter Form. Man unterscheidet zwischen sogenannten Tracing-Algorithmen und reference counting. Tracing-Algorithmen bestimmen von Wurzelobjekten (engl. root set) ausgehend, welche Objekte im Speicher noch erreichbar sind. Alle Objekte auf die das nicht zutrifft können verworfen werden, da sie definitiv nicht mehr gebraucht werden (sie sind ja nicht mehr referenzierbar). Die Wurzelobjekte können z.b. in einer Liste gespeichert sein oder durch Scannen des Stacks bestimmt werden. 3
Im Folgenden wird zuerst aber reference counting beschrieben. 4.1 reference counting Wahrscheinlich die offensichtlichste Methode automatisches Speichermanagement zu betreiben ist alle Pointer/Referenzen zu zählen, die auf den jeweiligen Speicherbereich zeigen. In Worten: Für jedes Objekt im Speicher halte die Anzahl der Pointer, die auf dieses Objekt zeigen, irgendwo fest. Wird ein Pointer einem anderen Pointer zugewiesen wie hier in C i n t p, q ; ; p = q ;, dann erhöhe den Zähler des Speicherobjekts auf das q zeigt und erniedrige den Zähler des Speicherobjekts auf das p zeigt. Ist ein Zähler null kann der jeweilige Speicherbereich freigegeben werden. Werden vorher noch alle Pointer in diesem Speicherbereich abgearbeitet und deren Zähler dekrementiert, spricht man von rekursivem reference counting. Diese Methode wurde zum ersten Mal von George E. Collins in 1960 beschrieben [3]. Reference counting hat einige Vorteile aufgrund seiner Einfachheit. Nicht mehr gebrauchte Speicherbereiche werden sofort wieder freigegeben, es wird nur auf die wirklich benötigten Pointer zugegriffen und der nötige Rechenaufwand verteilt sich über die gesamte Ablaufzeit eines Programms. Simples reference counting kann jedoch nicht mit zyklischen Datenstrukturen umgehen, d.h. Objekte die sich selbst referenzieren. Jedoch kann der Algorithmus erweitert werden um mit diesen umzugehen. Des weiteren kann des Erhöhen und Erniedrigen des Zählers signifikanten Overhead erzeugen, was jedoch wieder durch Optimierungen vermieden werden kann. Reference counting wird in C++ bei sogenannten smart pointers zur automatischen Speicherverwaltung verwendet [2]. 4.2 mark-sweep Bei mark-sweep handelt es sich um einen sogenannten tracing-algorithmus. Ausgehend von Wurzelobjekten (Pointer in Registern, Stacks, globalen Variablen) markiert es die Speicherbereiche die erreihbar sind. Diese Speicherbereiche werden wieder nach Pointern zu Speicherbereiche durchsucht und dann diese markiert, usw. In der zweiten Phase (sweep) werden dann alle 4
5 Schluss Literatur [1] malloc(3) - Linux man page. http://man7.org/linux/man-pages/man3/ malloc.3.html. [Online; accessed 10-December-2014]. [2] The GNU C++ Library. https://gcc.gnu.org/onlinedocs/libstdc++/, 2008-2014. [Online; accessed 17-December-2014]. [3] George E. Collins. A method for overlapping and erasure of lists. Communications of the ACM, 3(12):655 657, December 1960. [4] Richard Jones, Antony Hosking, and Eliot Moss. The Garbage Collection Handbook: The Art of Automatic Memory Management. CRC Applied Algorithms and Data Structures. Chapman & Hall, January 2012. [5] Richard E. Jones. Garbage Collection: Algorithms for Automatic Dynamic Memory Management. Wiley, Chichester, July 1996. With a chapter on Distributed Garbage Collection by R. Lins. [6] John McCarthy et al. Lisp 1.5 Programmer s Manual, 1962. 5