MP7: My, oh, my, oh, malloc!

CS 241, Spring 2010



This MP is only for EXTRA CREDIT! You may add up to six points to your final course grade by fully completing this MP. Your grade for the course will be calculated without respect to these extra credit points; after the grade is calculated, extra credit will be applied. Therefore, this MP does not have to be completed and will not hurt you relative grade if you choose not to do it. However, it's a lot of fun and we think it's well worth your time!

Introduction

In this MP, you will re-implement the function heap-memory function calls malloc(), calloc(), realloc(), and free() in much the same way that valgrind re-implements many C-calls to understand how your program is using memory. Just like valgrind, you will be able to use almost any existing program with your new memory functions: this includes utility programs such as "ls" or text editors "vi" or any other programs that runs on Linux.

Your first goal will be a basic correct solution. Then, you can move on to improving your allocator's speed and/or efficiency of memory use. And for fun and a bit of profit, you'll be able to submit your malloc to a class contest.

What you must do

In this MP, you will need to make working versions of malloc(), calloc(), realloc(), and free() without using the libc defined malloc(), calloc(), realloc(), and free() calls. Instead, you will interact directly with the heap via the sbrk() function call. For the purpose of this MP, you are not allowed to use any other form of storage. You may not use files, pipes, system shared memory, mmap, a large chunk of pre-defined static memory, a large chunk of pre-defined stack memory, other external memory libraries found on the Internet, or any of the various other external sources of memory that exist on a modern operating system. Your re-implement of malloc() must allocate heap memory using the sbrk() call.

We provide two files:

That's it! You now know the requirements for MP7. Note that we haven't specified how you should implement your memory allocator. You've learned a lot of different algorithms and data structures in CS 241 for how systems can manage memory and it's up to you to choose how to do it.

But don't worry; we'll provide some guidance. In the rest of this document we supply background to help you get started, and then discuss grading and the contest.

How to get started

A bad malloc

Memory allocation might seem a bit magical. But you might be surprised to find out how easy it is to write a memory allocator which actually works in some primitive way. Let's do it now. First, malloc():

void *malloc(size_t size)
{
  return sbrk(size);
}

How does that work? Malloc gets a request for size bytes of memory. It then calls sbrk(size). This is a system call that asks the operating system to increase the heap by size bytes. sbrk() returns a pointer to this new memory, and away we go. Now, how about free?

void free(void *ptr)
{
}

That's right! It's empty. In a very limited way, this is a "correct" implementation. You could write programs that use this malloc and free. So what's wrong with this implementation?

To sum up, it's actually quite easy to request new memory from the operating system. Your memory allocator's other task is harder: do the necessary "bookkeeping" to track what chunks of memory are allocated, and where the free holes are.

Towards a better malloc

So how do we do this "bookkeeping"? Let's see what information we need. When free(ptr) is called, its job is to free the block of memory starting at ptr. But how many bytes long was that block? Sadly, we are told where the memory segment starts but not how big it is. Similarly, take a look at realloc(ptr, size): it must "define a new memory segment, copy the old memory segment content to the new memory segment, free the old memory segment, and then return the new memory segment". We find that to do that, we need three pieces of information:

Sadly, realloc() has as arguments only the old memory location and the new memory size. So to sum up, for both free() and realloc() we must store some bookkeeping information behind the scenes when the memory is first allocated with malloc(). Specifically, we need some way to figure out the memory segment size given only the memory location ptr.

Let's define some structure to correlate the old memory location to the old memory size; that way we can look it up later:

typedef struct _mem_dictionary
{
  void *addr;
  size_t size;
} mem_dictionary;
...and let's use global variables to keep a count of how many records are part of this structure, as well as a pointer to the first element of the dictionary:
mem_dictionary *dictionary = NULL;
int dictionary_ct = 0;
With these data structures set up, we can make simple modifications to malloc() so it initalizes the dictionary array when it starts up and stores the (memory address)->(size) record every time malloc() is called:
void *malloc(size_t size)
{
  void *return_ptr = sbrk(size);
 
  if (dictionary == NULL)
    dictionary = sbrk(1024 * sizeof(mem_dictionary)); /* Note the use of sbrk() and not malloc(), since malloc() would create an infinite loop of calling malloc(). */
  dictionary[dictionary_ct].addr = return_ptr;
  dictionary[dictionary_ct].size = size;
  dictionary_ct++;
 
  return return_ptr;
}

Finally, we can create a realloc() function now that we have found a way to get access to the size of an existing memory segment:
void *realloc(void *ptr, size_t size)
{
  void *return_ptr = malloc(size);
 
  if (!ptr)
    return return_ptr;
 
  size_t old_size = 0;
  int i;
  for (i = 0; i < dictionary_ct; i++)
    if (dictionary[i].addr == ptr)
      old_size = dictionary[i].size;
 
  memcpy(return_ptr, ptr, old_size);
  free(ptr);
 
  return return_ptr;
}
...and, at this point, you have something closer to a working program. One of the remaining problems is that free() still does not mark memory that was allocated to the expanding stack to be reused (eg: a program that grabs 1 MB and then frees the 1 MB will run out of memory since each 1 MB given isn't the same 1 MB, but a different 1 MB). Also, the dictionary contains a fixed number of entries that will result in a segfault if malloc() is called over 1,024 times. Also, malloc() has still done nothing to attempt to intelligently reuse memory—which is where algorithms like First Fit and Best Fit would be used. Other inefficiencies exist in the code, too, that you may want to optimize as part of the contest element of this code.

Note that we do not mean to imply that you should even begin with the dictionary-based strategy above. For example, a common trick is to integrate the bookkeeping, or metadata, with the memory segments themselves. Specifically, when you get an allocation request for size bytes you can instead reserve a few bytes more. The first part of this memory is for your metadata; malloc then returns a pointer to the memory that comes after the metadata. But when free(ptr) is called, it can figure out where the metadata is based on ptr. You can then use the metadata space to, for example, store the length of the segment and pointers for a linked list of segments.

How to test your allocation functions

To compile the launcher that we provide, along with your re-implementation of malloc, run:
make clean
make
After a successful make, you will have compiled your malloc() as a shared library object (.so) and can use the launcher to launch any Linux program using your allocation library. To do so, run:
./mreplace <Program Name> [<Program Args> ...]
For example:
./mreplace /bin/ls
./mreplace /bin/ps -aef
./mreplace anyprogram
...
You will find that valgrind will not work on this program! The ./mreplace program works in the exact same way as valgrind, so running a command like valgrind ./mreplace /bin/ls will result in ./mreplace replacing the valgrind-defined malloc() call, just like valgrind replaced toe libc malloc() call. Since you control the entire memory allocation in this MP, it doesn't even make sense to even test if you've correctly free'd all of your memory. Therefore, the grading of this MP will be based entirely on execution.

Suggested plan

Start by trying out the "bad malloc" above. Then think about the data structures and algorithms you'll use for your bookkeeping. We suggest you get a simple solution up and running first. Then for extra points, you can think about how to make your solution simultaneously fast and efficient with memory use.

Grading

You can earn up to six extra points to your final course grade in CS 241. The points are broken down as follows:

Grading for correctness

To test the correctness of your program (3 points), we provide five testers: To run the testers to examine the expected output, simply run the testers from the command line:
% ./tester-1
Memory was allocated, used, and freed!
%
A correct re-implementation of the functions required in the MP should result in running the same command inside ./mreplace resulting in the same output:
% ./mreplace ./tester-1
Memory was allocated, used, and freed!
%
Each output that matches correctly will award you 0.60 points to your final CS 241 grade for a maximum total of 3.00 points.

Grading for efficiency

While there are a number of metrics that a system library designer must be concerned with, we have chosen to look at three of the most important metrics to measure the efficiency of your program: Instead of running with ./mreplace, a second executable file has been provided for you to examine the efficiency of your program: ./mcontest. Rather than simply replacing malloc() and other calls with the ones you've programmed in alloc.c, ./mcontest monitors your program's memory usage and records interesting statistics. Using ./mcontest works just like ./mreplace, except ./mcontest will print efficiency before it exits. For example:
% ./mcontest ./tester-4
Memory was allocated, used, and freed!
[mcontest]: STATUS: OK
[mcontest]: MAX: 1359776200
[mcontest]: AVG: 2191596.819109
[mcontest]: TIME: 20.552874
%
As reported by ./mcontest, the program makes use of 1,359,776,200 bytes, or 1.266 GB, of heap memory at its maximum and took a little over 20 seconds to run. Running a few sample programs we used while debugging this MP, here are some results:
libc % algo-1 % algo-2 % algo-3 %
tester-2 MAX 1761280 100% 3284992 186.51% 2423897 137.62% 2423897 137.62%
tester-2 AVG 1344568.855604 100% 2641054.273817 196.42% 1957856.440237 145.61% 1957856.440237 145.61%
tester-2 TIME 0.45992 100% 0.49992 108.70% 13.168997 2,863.32% 5.439260 1,088.02%
100% 163.88% 1,048.85% 457.08%

To assign extra credit for efficiency, we will look at how well your program runs relative to libc. Specifically: ...remember, the table above shows only tester-2. We will run all five testers and other real-life applications to test the speed and robustness of your library. For your reference, we provide a sample run of the contest when using ordinary malloc(). You will see how your program compares to malloc() on the contest results page:
% ./mcontest tester-1
Memory was allocated, used, and freed!
[mcontest]: STATUS: OK
[mcontest]: MAX: 139264
[mcontest]: AVG: 139264.000000
[mcontest]: TIME: 0.36994

% ./mcontest tester-2
Memory was allocated, used, and freed!
[mcontest]: STATUS: OK
[mcontest]: MAX: 1761280
[mcontest]: AVG: 1344568.855604
[mcontest]: TIME: 0.45992

% ./mcontest tester-3
Memory was allocated and freed!
[mcontest]: STATUS: OK
[mcontest]: MAX: 1249280
[mcontest]: AVG: 1248139.076607
[mcontest]: TIME: 0.60990

% ./mcontest tester-4
Memory was allocated, used, and freed!
[mcontest]: STATUS: OK
[mcontest]: MAX: 40046592
[mcontest]: AVG: 24309281.808070
[mcontest]: TIME: 0.178972

% ./mcontest tester-5
Memory was allocated and freed!
[mcontest]: STATUS: OK
[mcontest]: MAX: 129695744
[mcontest]: AVG: 68332791.881479
[mcontest]: TIME: 0.321950

Grading for contest rank

The final point of extra credit that can be earned comes from beating out other students in "the malloc contest". Specific details on how this point of extra credit will be provided in the future; the contest will be rated on the averaged percentage performance relative to libc discussed in the previous section; the higher you place, the greater the fraction of the last extra credit point you will be awarded.

The malloc contest

"The malloc contest" looks to pit your re-implementation of memory allocating functions against your fellow students. There are a few things to know: