PLI Lecture 9 Run-time environments III: Data representation and dynamic storage management 1. DATA REPRESENTATION Primitive data types: bool, char, int, double, etc. - May be determined by language definition: 2-byte Unicode characters, 32-bit 2s-complement ints, 64-bit IEEE doubles, etc. Arrays: - One-dimensional arrays, e.g. an array of 10 T's, indexed from 0. Suppose sizeof(T) = s. Then, addr(a[i]) = addr(a) + i*s (All arrays in Java are one-dimensional.) - Two-dimensional arrays, e.g., T a; Suppose the array is stored in row-major order. Then, addr(a[i,j]) = addr(a[0,0]) + (i*20 + j)*s - Exercise: Write the corresponding formula for three-dimensional arrays. - Compexities occur if the array sizes are only known at run-time, e.g., T a[M][N]; Structures (records): - Fields of structures are stored contiguously, aligned according to their type. In some languages, order needs to be preserved. In some cases, structures must be packed (i.e., no gaps). - Represent structures in same way as activation records. Unions: - Allocate enough space for largest variant. Class instances: - Storage for class instances (and arrays in Java) is created dynamically and stored in the heap. - Each class instance is (primarily) a structure, and stores the instance fields of the class, TOGETHER with the instance fields of its superclass, and so on. - Pointers to instance methods (of the class and its superclasses) MAY be stored with each class instance. But this is wasteful of space. - Alternatively, store the class hierarchy in memory during execution, with method names and pointers to instance methods at each node. Also, store a pointer in each instance of class C to the node for class C. - Polymorphism and dynamic binding. Now, to find the method definition corresponding to a method name for a class instance, follow the class pointer in the instance and then follow the class hierarchy upwards until the first class with a definition for the method is found. - Dynamic (and static) binding can be implemented more efficiently using virtual function tables (see Example 7.8). 2. HEAP (DYNAMIC STORAGE) MANAGEMENT This is a very complex issue on which much research has been done without any dominant solution being found. I.e., which solution is best depends on the circumstances! See: http://en.wikipedia.org/wiki/Dynamic_memory_allocation and D.E. Knuth. Fundamental Algorithms, Third Edition. Addison-Wesley (1997), Section 2.4: Dynamic Storage Allocation, pp.435-456. The heap is a contiguous (linear) block of memory. Blocks of storage are allocated in the heap with a method new() or malloc(), and are returned with a method free() or dispose(). In Scheme and Java, inaccessible blocks are automatically returned by a garbage collection process. Desirable goals to consider in implementing a heap management scheme include: - Fast allocation and return. - Little internal fragmentation (wasted space in allocated blocks). - Little external fragmentation (many small, noncontiguous, unusable free blocks in the heap) - Robustness 2.1 One method for explicit heap management (Louden, 7.4.3) Maintain a circular list of blocks. Each block has a header, some allocated space, and some free space. The header contains a pointer to the next block, the size of the allocated space (if any), and the size of the free space (if any). Initially, there is a single block containing the whole heap, whose next pointer points to itself, with no allocated space. At any time, a start pointer (in a register) points to a block that has some free space (initially the large single block). To allocate a block, we search from the start pointer for the first block with enough free space. We create a new block from the free space of the old block, link the reduced old block to the new block, and the new block (which contains the remaining free space of the old block) to the old block's successor. The start pointer is set to the newly allocated block. (Searching for the "first" block above is the "first fit" heuristic. Sometimes "best fit" and even "worst fit" heuristics are used.) To free a block, we first find the block which points to this block. (We could either follow the circular list, or use a "previous" pointer in a doubly-linked list.) We then coalesce this block with the previous block, adding this block's used and free space to the previous block's free space, and updating the previous block's header. Finally, we set the start pointer to the newly coalesced block. See Louden, Fig. 7.19 for details. This is a simple method that illustrates some basic ideas, which attempts to avoid external fragmentation by coalescing blocks on return, but whose performance is not guaranteed. 2.2 Garbage collection (Louden, 7.4.4) This is also a much studied topic, with many implementations. - Mark-sweep garbage collection. When storage is exhausted, recursively traverse all blocks in the heap (starting from stack pointers), marking each block reached. Then linearly traverse the blocks in the heap, returning each unmarked block to free memory. A third pass may be required to coalesce many small free blocks. A technical problem with this method is that there may not be free memory available to store the stack required for the recursive traversal! Subtly data structures may be used to store the stack as pointers in the heap blocks themselves. A more fundamental problem is that all other computation must cease while garbage collection is performed. - Reference counters. Store the number of references to each block in the header of the block. When this counter becomes zero, return the block to free memory. This method has a problem with cyclic structures! - Stop-copy garbage collection. Divide available memory for the heap into two equal halves. Allocate storage from one half at a time. When this half is exhausted, recursively traverse all used blocks as before, copying each used block into the other half. Once the traversal is complete, reverse the roles of the two halves. This avoids the need for a mark bit, but requires more storage (which is becoming cheaper) and still requires other computation to cease during garbage collection. - Generational garbage collection. This idea records how long blocks have been in use and avoids traversing long-lived blocks, hence traversing smaller lists, making collection faster. Heuristic, but effective in many cases. - Parallel garbage collection. With a shared memory multiprocesor node, it is possible to allocate one processor to collect garbage while other processors continue actual computation. The details are complex (several processors may be allocating and freeing blocks while the distinguished processor is collecting), but the benefits are significant. See: http://en.wikipedia.org/wiki/Garbage_collection_(computer_science) and R. Jones and R. Lins, Garbage Collection: Algorithms for Automated Dynamic Memory Management, Wiley (1996).