5.1 Allocator

Hierarchy in the slides: pages heap blocks payload.

  • the allocator get’s pages (or less) from the OS using mmap and sbrk (extend heap)
    • it can only use what’s inside there
  • in the heap, it can split that up into blocks
  • the block contains the management data (i.e. size) + payload (which is what the program actually gets)

Allocator:

  • is not allowed to use malloc call for defrag → must respond immediately

5.2 Fragmentation

Internal vs. External Fragmentation

Example Internal Fragmentation

Example External fragmentation

5.3 Freeing

How much to free

We keep the length of a block in a head word, just ahead of it (header field).

On free, we then read this.

Keeping track of Free Blocks

To keep track of free blocks, we can:

  • implicit list store the length of the free blocks (as a header)
  • explicit list use the free space to keep a linked list with a pointer to the next free node
  • or other…

5.4 Implicit Free Lists

Properties Summary

  • very simple implementation
  • allocation linear runtime in the worst-case
  • free constant time worst case, even with coalescing
  • memory usage
    • depends on placement policy
    • first-fit, next-fit, best-fit
  • not used in practice → linear allocation is too bad

Want to store the length and allocated in one word

  • use the last bit as a allocated flag
    • mask out the bit with *p & -2 (as -2 has last bit = 0)
  • can only get even aligned addresses → is ok

How to find the open space:

  • first fit
    • search list from beginning, choose first free block that fits
      • usually causes “splinters” at start of list
  • next fit
    • start where we last left off → faster since we don’t need to rescan
    • fragmentation may be worse
  • best fit → scan them all
int *p = start;
while ((p < end) && (
		(*p & 1) || // already allocated
		(*p <= len) // too small
	)) {
	p = p + (*p & -2)
}

Allocating a new block:

5.4.1 Coalescing in Implicit Free List

Freeing a block should be as simple as “clear the allocated bit”, but this leads to false fragmentation!

  • We get multiple chunks which are free, but not in one block
    • when we then look for free memory, there might be enough space, but the allocator won’t find it

We need Coalescing → we want to merge two adjacent free blocks.

5.4.2 Bidirectional Coalescing (Knuth)

This is not possible with only a header bit → we need the footer to indicate the size of the block at the end.

Note: this style of header / footer leads to internal fragmentation (i.e. unused space at start and end) → with alignment cost on top

Then we can “doubly linked list” jump around.

coalescing → four possible cases

  • this cannot cascade, since we can only free one block per call
    • we always coalesce at free → no buildup

5.4.3 Allocator Policies

Do we do:

  • immediate coalescing → coalesce each time free is called
  • deferred coalescing → try to improve free performance by deferring until needed
    • coalesce as you scan via malloc
    • coalesce when external frag reaches threshold

We can use segregated free lists (one list per “size” threshhold) to approximate best fit placement without searching entire list.

5.5 Explicit Free Lists

With the implicit list, we noticed that the header/footer tags are oonly needed for non-allocated blocks!

We can thus build an explicit free list, which is a doubly-linked-list of only free blocks.

Since we don’t impose any constraints on the ordering, they will be all over the place in memory.

  • not cache efficient?

5.5.1 Allocating

When allocating, once we found a block large enough:

  • move the pointers to the “end of the allocated block” → i.e. new start of free section
  • relink the linked list + adjust metadata

5.5.2 Freeing

For freeing, we need to decide where in the list to put the newly freed block

  • LIFO
    • insert at beginning of freed list (simple + constant time)
    • empirical: fragmentation worse than address ordered
  • Adress Ordered
    • insert freed-block so free-list is always in address order
      • addr(prev) < addr(curr) < addr(next)
    • requires search but fragmentation better.

LIFO Cases:

  • no coalescing, just splice in new pointers to the block at the start
  • free block before/after
    • when freeing → always check word before/after to see if there’s free block
      • we know this from the header/footer
    • splice out the other block
    • modify headers for new size and relink at start
  • free block before AND after
    • splice out both pred and succ blocks → coalesce all three
    • insert + relink at the start

5.5.3 Summary

Comparison to implicit:

  • allocate is linear to free blocks vs. all blocks (for implicit)
    • much faster when memory is full
  • slightly more complicated
  • extra space needed

most commonly used: explicit + segregated free lists.

5.6 Segregated Free (“seglist”) List

Each size class of blocks has it’s own free list. Keep lists of (1-2, 3, 4, 5-8, 9-inf or similar, ex: one class for each power of two). Then we can find blocks of the necessary size faster.

5.7 Garbage Collection

GC is automatic reclamation of heap-allocated storage → application never has to free.

Example Memory becoming inaccessible

void foo() {
	int *p = malloc(128);
	return; // from here on out, p is now garbage collectible -> not accessible anymore
}

Assumptions necessary for GC:

  • must be able to differentiate pointers from non-pointers → no explicit pointer manipulations
    • if you can cast int → pointer, you can always access anything
    • thus no GC possible
  • all pointers point to start of block
    → C violates both of this

GC views memory as a directed graph

  • root nodes (always accessible, like modules, statics, globals, etc…)
  • heap nodes (accessible from root nodes)
    • collect them when they are not connected anymore
    • must detect cycles

Mark & Swap:

  • DFS from root nodes, mark all reachable
  • then dealloc all non-marked
  1. Dereferencing bad pointers
    1. make sure what you dereference is a pointer
    2. int val, scanf("%d", val) → scanf requires an address, here it writes into whatever val was uninitialised as
  2. Don’t assume heap data is initialised to zero
  3. Overwriting memory through mallocing
    1. if you have 2d-array (i.e. n pointers to int array) and alloc sizeof(int) not sizeof(*int)
  4. Overwriting memory
    1. char s[8] buffer and gets(s) → if someone gives large input we have buffer overflow
    2. overwrite meaningful data after the buffer.
  5. Understand pointer arithmetic
    1. when going in an array / buffer
    2. p += sizeof(int) is wrong → pointer arithmetic already increments by pointer datatype size
      1. this is what we used to find LE vs. BE by casting to char *
  6. Referencing non-existent variables
    1. local variables = stack → disappear when function returns
    2. int * foo() { int val; return &val; } → &val will not be what we think it is on return
      1. stackframe got yoinked
  7. Double freeing OR referencing freed blocks OR never freeing
    1. Classic memory leak
      1. foo() { int *x = malloc(...); return; } → can never free x again, since we lost access to it as it was on stack
    2. Don’t free start of DSA → if we free head pointer of Linked-List, can never free the rest
      1. need to loop over
  8. Order of operations
    1. *size-- is *(size--) !