Evaluator for Prelim is now on the web. Review session will be section on Monday. PS5B will be out later today.
Circular datastructures are actually not needed for mutual recursion (just delays…) But they are needed for, e.g., airline routes.
Memory and pointers and indirection
Byte versus word addressing (Pentium is 32 bit, byte addressed).
Alignment and word operations versus byte operations (word operations are more efficient)
The memory hierarchy – pretending we have a large fast memory when in fact we have a small fast memory and a large slow memory. This happens at many levels (about 4 times within a Pentium, then main memory, then in the disk drive, etc.) Importance of locality, spatially and temporally.
SML runtime only has 4 types: integers, pointers, records and strings. A field is either a pointer or an integer. We determine the difference by using the low order bit, since pointers are word-aligned.
Note that we will assume that given a pointer it is easy to determine the type of the object it points to. How can we do this? The easy way is to use the high-order bits (why not the low order?) This scheme is called BIBOP.
SML doesn’t use BIBOP (you will see why shortly); instead, a record is represented as [Record type tag,length,elt1,elt2,…] In the actual implementation, tag and length are compressed into a single 32-bit word.
Strings are usually 1 byte and null-terminated. Quick overview of string operations. SML strings, unlike C strings, are word-aligned. “Wide” strings and Unicode (source of many bugs!)
The list [6,9,42] will be [CONS,6,ptr] [CONS,9,ptr] [CONS,42,ptr]. Note that this allows you to easily create new lists that share structure with old lists (for example, consider what x::l does…)
A ref is just a memory location holding a pointer
A tuple is a sequence of memory locations (there is some freedom about how to represent a nested tuple).
An environment might be an array of known size, where each element is a string pointer (to the name) and a value (which may in turn be a pointer). An actual compiler will get rid of the names and replace them with offsets.
A critical issue in terms of performance is memory management: efficient use of memory. To do this really right requires understanding a lot about the particular hardware you will run on, but there are some general purpose issues. One of the key ones is locality.
The run-time system for SML needs to be able to allocate memory. So far we have pretended that there is an infinite set of values. Obviously, we can’t allocated an array of size 10^100… The flip side of this is that we also need to be able to deallocate memory – reclaim it when it is no longer in use. These two topics will occupy the next few lectures.
More generally, memory management is concerned with issues like:
· finding memory for a new variable or value
· avoiding putting two values in the same place
· avoiding leaving memory unused
· reusing memory if the value stored there can no longer be accessed
Heap allocation versus stack allocation
Sometimes you can tell that a variable won’t be used after the current procedure call returns. Values like this are “stack allocated”, which essentially means that they are created to automatically disappear on the return.
Anything that might persist longer is “heap allocated”. Note that the memory heap is not the same as a heap datastructure (as used in a priority queue).
How do we do heap allocation (and deallocation)? Explicit allocation (malloc) and deallocation (free) this lecture, automatic deallocation (GC) next lecture.
Allocation simple case: fixed sized blocks of storage. Create a freelist of unused blocks of memory in a linked list. First word of the block has a pointer to the next element in the freelist. On malloc, set free pointer to where this block points. On free, cons this block in front of what used to be the freelist.
Problem: fixed size blocks don’t work. Inefficient when what you need is smaller, impossible when it is bigger. Solution: variable sized blocks.
The obvious solution is to stick in the block its size. Then we look down the freelist for something bigger than what we need, and when we get there we return the size of memory we need, and replace this in the freelist by the remainder.
Problem: worst case we walk the entire list. And we can potentially get lots of small blocks (aka external fragmentation (internal is boring)).
Or you could have different freelists for different sizes. Note that if we use BIBOP we essentially need one freelist per type. This is a total pain! Your program can run out of, e.g., floating point numbers but still have tons of room for Booleans, etc. This is part of why SML doesn’t use BIBOP. In fact, as this example suggests, a sufficiently performance-intensive application always has its own memory allocator.
Popular allocation method: buddy system. In the standard buddy system (there is also a fibonacci buddy system), there are fixed size blocks for powers of 2. The actual invariant is that you need to have a set of fixed sizes, where you can split a bigger block into two blocks of smaller sizes.
If you are out of blocks of size 4, you can split a block of size 8 in two; one will be returned, the other added to the freelist of size 4. Furthermore, when a block is freed if it is right next to a block of the same size (its “buddy”) the two of them can be merged to create a larger block. There is still a cost in terms of internal fragmentation because the free block sizes are “sparse”. This is an advantage of the fibonacci buddy system.
There are two basic strategies for dealing with
garbage: explicit garbage collection by the programmer, and automatic garbage
collection built into the language run-time system. Explicit garbage collection
is provided by languages like C and C++. There is a way to explicitly
deallocate (or "free") allocated memory when it is expected that that
memory is about to become garbage. Languages like Java and SML provide automatic
garbage collection : the system automatically identifies blocks of memory that
can never be used again by the program, and reclaims their space for use by
later allocations.
Automatic garbage collection offers the advantage
that the programmer does not have to worry about when to deallocate a given
block of memory. In languages like C the need to explicitly manage memory
complicates any code that allocates data on the heap, and is a significant
burden on the programmer. Worse, if the programmer fails to deallocate
properly, bugs are introduced into the program that are hard to find:
In practice, programmers manage explicit allocation
and deallocation by keeping track of what piece of code "owns" each
pointer in the system. That piece of code is responsible for deallocating the
pointer later. The tracking of pointer ownership shows up in the specifications
of code that manipulates pointers, complicating specification, and use, and
implementation of the abstraction.
Automatic garbage collection helps modular
programming, because two modules can share a value without having to agree on
which module is responsible for deallocating it. The details of how boxed
values will be managed does not pollute the interfaces in the system.