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[0]) + i*s
  (All arrays in Java are one-dimensional.)
- Two-dimensional arrays, e.g., T a[20][10];
  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).