Lecture 14: Managing the Heap

Table of contents:


We will discuss how malloc/realloc/free work–specifically their implementation. Pulls together all of course topics thus far.

Learning goals:


1. The heap so far

Our role so far: client

It is vital that you recall what these functions do (see the man page here):

Heap allocation functions:
  • malloc: a function that allocates size bytes on the heap and returns a pointer to the starting address of the allocated memory.
void *malloc(size_t size); 
  • realloc: a function that changes the size of the memory block pointed to by ptr to size bytes.
void *realloc(void *ptr, size_t size);
  • free: a function that frees the memory space pointed to by ptr (must be heap memory that is being pointed to).
void free(void *ptr); 

Our new role: heap allocator

Heap allocator: A heap allocator is a set of functions that manages and fulfills requests for heap memory.

By “requests” we mean calls to malloc, realloc, and free.

But what is the heap?

Heap: The heap (as is all memory actually) is simply a large contagious block of memory indexed byte-wise which persists throughout the program runtime (i.e. no frames that go away after function calls as is seen with stack memory).

Imagine that this diagram is the heap:

A heap allocator implementation must manage this block memory as clients request or no longer need parts of it.


2. Requirements and goals of a heap allocator

Let us dive into some details.

Requirements

Some core requirements that a heap allocator must adhere too:

  1. Handle arbitrary request sequences of allocations and frees.
    • The HA cannot assume anything about the order of allocation and free requests. Even more, it cannot assume that every allocation is accompanied by a matching free request.
  1. Must keep track/mark which memory slots are already allocated and which are available.
  1. Decide which memory block to provide to fulfill an allocation request.
    • This is hugely important and spans most of what we will study (e.g. should we priotize memory space or time?).
  1. Immediately respond to requests without delay.
    • i.e. no waiting to “batch” requests. No reordering to make more efficient.
  1. Return addresses for allocation requests must be 8-byte aligned (i.e. each address given is a multiple of 8).
    • One reason for this is for efficiency (for things like search).
    • Another very important reason is the bit pattern for numbers that are multiples of 8. This is important for implementing the implicit allocator which we introduce later.
    • To confirm your understanding, recall that calls to the C standard library functions for allocation requests return an address (i.e. pointer).

Goals

Two core goals which are negatively correlated.

Goals for a heap allocator:
  • Goal 1: maximize throughput which is the number of requests completed per unit time (i.e. time efficiency).
  • Goal 2: maximize memory utilization, or how efficiently we make use of the limited heap memory we have to satisfy requests (i.e. memory efficiency).

Fragmentation

The primary cause of poor heap utilization is a phenomenon known as fragmentation which we discuss here.

Let us discuss more about the second goal and some common scenarios people encounter. Say we have the following scenario:

Here, the client is asking for 4 bytes but the allocator does not have a contagious 4 byte block. However, there are more than 4 bytes free… just not contagious. This inefficiency is known as fragmentation which occurs when otherwise unused memory is not available to satisfy allocation requests.

So how do we solve this? If you think about it, in general, we want the the largest (i.e. highest... not largest byte-wise) address used to be as low as possible.

To do this we might want to manually shift the blocks down to make more space. Like so:

But we actually cannot do this. This is because when a client makes an allocation request, the HA returns an address to the block in memory. So shifting a bunch of blocks around will ruin prior addresses given (i.e. incorrect pointers).

We can further divde the problem of fragmentation into two subproblems:


3. Bump Allocator

Ok... now that we understand the goals and requirements of a heap allocator, we will implement one. There are many different design optimizations one can make when it comes to designing heap allocator implementations (e.g. should we prioritize space or time efficiency?) so indeed, there are many different types of implementations. We will cover three such implementations where the first one is a "baby" implementation with poor design but easy to implement. It is called a bump allocator (which we will abbreviate BA here).

A bump allocator is a heap allocator design that simply allocates the next available memory address upon an allocate request and does nothing on a free request.

4. Implicit Allocator

Looking above, we see the inefficiencies of the bump allocator. We asked questions about things we can potentially do to make our allocator more efficient. We will harness these ideas to build a more efficient allocator called the implicit allocator. Specifically, we will find solutions to the four questions above.

One downside is that you need to loop through all blocks to find a free block. To only loop through free blocks, we can make a doubly linked list.