In this lab you will be writing a dynamic storage allocator for C programs, i.e., your own version of the malloc, free and realloc routines. It is quite involved. Start early!
This is an individual project.
WARNING: Do not let the Windows WinZip program open up your .tar file (many Web browsers are set to do this automatically). Instead, save the file to your Linux directory and use the Linux tar program to extract the files. In general, for this class you should NEVER use any platform other than Linux to modify your files. Doing so can cause loss of data (and important work!).
Try:
#cp
~liqun/public_html/teaching/cs304_15f/labs/malloclab-handout.tar.gz
.
#tar xvf malloclab-handout.tar.gz
This will cause a number of files to be unpacked into the directory. The only file you will be modifying and handing in is mm.c. The mdriver.c program is a driver program that allows you to evaluate the performance of your solution. Use the command make to generate the driver code and run it with the command ./mdriver -V. (The -V flag displays helpful summary information.)
Write your name and login ID in mm.c. There is a line for a team member. Please ignore it, but do not remove it.
When you have completed the lab, you will hand in only one file (mm.c), which contains your solution.
a machine in
MS-121
. For this lab, you can work anywhere there's a C
compiler and make
, but make sure your allocator works
on machines in MS-121
, where we'll be testing it.)
Your dynamic storage allocator will consist of the following
functions (and several helper functions), which are declared in mm.h
and defined in mm.c
:
int mm_init(void);
void* mm_malloc(size_t size);
void mm_free(void* ptr);
void mm_realloc(void* ptr, size_t size);
The mm.c
file we have given you partially
implements an allocator using an explicit free list. Your job is
to complete this implementation by filling out mm_malloc()
and mm_free()
. The main memory management functions
should work as follows:
mm_init()
(provided): Before calling mm_malloc()
or mm_free()
, the application program (i.e., the
trace-driven driver program that you will use to evaluate your
implementation) calls mm_init to perform any necessary
initializations, such as allocating the initial heap area. The
return value is -1 if there was a problem in performing the
initialization, 0 otherwise.mm_malloc()
: The mm_malloc()
routine returns a pointer to an allocated block payload of at
least size
bytes. (size_t
is a type
for describing sizes; it's an unsigned integer that can
represent a size spanning all of memory, so on x86_64 it is a
64-bit integer.) The entire allocated block should lie within
the heap region and should not overlap with any other allocated
block. mm_free()
: The mm_free()
routine
frees the block pointed to by ptr
. It returns
nothing. This routine is only guaranteed to work when the passed
pointer (ptr
) was returned by an earlier call to mm_malloc()
and has not yet been freed. These semantics match the the
semantics of the corresponding malloc and free routines in libc.
Type man malloc
in the shell for complete
documentation. mm_realloc
should
also be performant. Avoid copying memory if possible, making
use of nearby free blocks.We will compare your implementation to the version of malloc supplied in the standard C library (libc). Since the libc malloc always returns payload pointers that are aligned to 8 bytes, your malloc implementation should do likewise and always return 8-byte aligned pointers.
We define a BlockInfo
struct designed to be used as
a node in a doubly-linked explicit free list, and the following
functions for manipulating free lists:
BlockInfo* searchFreeList(int reqSize)
: returns a
block of at least the requested size if one exists (and NULL
otherwise). void insertFreeBlock(BlockInfo* blockInfo)
:
inserts the given block in the free list in LIFO manner. void removeFreeBlock(BlockInfo* blockInfo)
:
removes the given block from the free list. In addition, we implement mm_init
and provide two
helper functions implementing important parts of the allocator:
void requestMoreSpace(int incr)
: enlarges the
heap by incr
bytes (if enough memory is available
on the machine to do so). void coalesceFreeBlock(BlockInfo* oldBlock)
:
coalesces any other free blocks adjacent in memory to oldBlock
into a single new large block and updates the free list
accordingly. Finally, we use a number of C Preprocessor macros to extract
common pieces of code (constants, annoying casts/pointer
manipulation) that might be prone to error. Each is documented in
the code. You are welcome to use macros as well, though the ones
already included in mm.c
are the only ones we used
in our sample solution, so it's possible without more. For more
info on macros, check the GCC manual.
FREE_LIST_HEAD
: returns a pointer to the first
block in the free list (the head of the free list). UNSCALED_POINTER_ADD
and UNSCALED_POINTER_SUB
:
useful for doing pointer arithmetic without worrying about the
size of BlockInfo struct. Additionally, for debugging purposes, you may want to print the contents of the heap. Here's code for a procedure to do so:
/* Print the heap by iterating through it as an implicit free list. */
static void examine_heap() {
BlockInfo *block;
/* print to stderr so output isn't buffered and not output if we crash */
fprintf(stderr, "FREE_LIST_HEAD: %p\n", (void *)FREE_LIST_HEAD);
for(block = (BlockInfo *)UNSCALED_POINTER_ADD(mem_heap_lo(), WORD_SIZE); /* first block on heap */
SIZE(block->sizeAndTags) != 0 && block < mem_heap_hi();
block = (BlockInfo *)UNSCALED_POINTER_ADD(block, SIZE(block->sizeAndTags))) {
/* print out common block attributes */
fprintf(stderr, "%p: %ld %ld %ld\t",
(void *)block,
SIZE(block->sizeAndTags),
block->sizeAndTags & TAG_PRECEDING_USED,
block->sizeAndTags & TAG_USED);
/* and allocated/free specific data */
if (block->sizeAndTags & TAG_USED) {
fprintf(stderr, "ALLOCATED\n");
} else {
fprintf(stderr, "FREE\tnext: %p, prev: %p\n",
(void *)block->next,
(void *)block->prev);
}
}
fprintf(stderr, "END OF HEAP\n\n");
}
The memlib.c
package simulates the memory system
for your dynamic memory allocator. In your allocator, you can call
the following functions (if you use the provided code for an
explicit free list, most uses of the memory system calls are
already covered).
void* mem_sbrk(int incr)
: Expands the heap by incr
bytes, where incr
is a positive non-zero integer
and returns a pointer to the first byte of the newly allocated
heap area. The semantics are identical to the Unix sbrk
function, except that mem_sbrk
accepts only a
positive non-zero integer argument. (Run man sbrk
if you want to learn more about what this does in Unix.) void* mem_heap_lo()
: Returns a pointer to the
first byte in the heap void* mem_heap_hi()
: Returns a pointer to the
last byte in the heap. size_t mem_heapsize()
: Returns the current size
of the heap in bytes. size_t mem_pagesize()
: Returns the system's page
size in bytes (4K on Linux systems). The driver program mdriver.c
in the lab file
distribution tests your mm.c
package for
correctness, space utilization, and throughput. Use the command make
to generate the driver code and run it with the command ./mdriver -V
(the -V
flag displays helpful summary information as
described below).
Each trace file contains a sequence of allocate and free
directions that instruct the driver to call your mm_malloc
and mm_free
routines in some sequence. The driver
and the trace files are the same ones we will use when we grade
your submitted mm.c
file.
The mdriver
executable accepts the following
command line arguments:
-t <tracedir>
: Look for the default trace
files in directory tracedir
instead of the default
directory defined in config.h
. -f <tracefile>
: Use one particular tracefile
for testing instead of the default set of tracefiles. -h
: Print a summary of the command line
arguments. -l
: Run and measure libc
malloc
in addition to the student's malloc package. -v
: Verbose output. Print a performance
breakdown for each tracefile in a compact table. -V
: More verbose output. Prints additional
diagnostic information as each trace file is processed. Useful
during debugging for determining which trace file is causing
your malloc package to fail. mm.c
.malloc
,
calloc
, free
, realloc
,
sbrk
, brk
or any variants of these
calls in your code. (You may use all the functions in memlib.c
,
of course.) static
compound data structures such as arrays, structs, trees, or
lists in your mm.c
program. You are
allowed to declare global scalar variables such as integers,
floats, and pointers in mm.c
, but try to keep
these to a minimum. (It is possible to complete the
implementation of the explicit free list without adding any
global variables.) malloc
implementation
in libc
, which returns blocks aligned on 8-byte
boundaries, your allocator must always return pointers that are
aligned to 8-byte boundaries. The driver will enforce this
requirement for you. Your grade will be calculated (as a percentage) out of a total of 60 points as follows:
mm_malloc
but not yet freed via
mm_free
) and the size of the heap used by your
allocator. The optimal ratio is 1. You should find good
policies to minimize fragmentation in order to make this
ratio as close as possible to the optimal. P = 0.6U + 0.4 min (1, T/Tlibc)
where U is your space utilization, T is your throughput, and Tlibc
is the estimated throughput of libc
malloc on your
system on the default traces. The performance index favors space
utilization over throughput. You will receive 5(P+ 0.1) points,
rounded up to the closest whole point. For
example, a solution with a performance index of 0.63 or 63% will
receive 4 performance points. Our complete version of the
explicit free list allocator has a performance index between 0.7
and 0.8; it would receive 5 points. Observing that both memory
and CPU cycles are expensive system resources, we adopt this
formula to encourage balanced optimization of both memory
utilization and throughput. Ideally, the performance index will
reach P = 1 or 100% . To receive a good performance score, you
must achieve a balance between utilization and throughput. mdriver
-f
option. During
initial development, using tiny trace files will simplify
debugging and testing. We have included two such trace files (short1-bal.rep
and short2-bal.rep
) that you can use for initial
debugging. mdriver
-v
and -V
options. The -v
option will give you a detailed
summary for each trace file. The -V
will also
indicate when each trace file is read, which will help you
isolate errors. gcc -g
and use gdb
.
The -g
flag tells gcc
to include
debugging symbols, so gdb
can follow the source
code as it steps through the executable. The Makefile
should already be set up to do this. A debugger will help you
isolate and identify out of bounds memory references. You can
specify any command line arguments for mdriver
after the run
command in gdb
e.g. run
-f short1-bal.rep
. fprintf
to print to stderr
is
helpful here because standard error is not buffered so you will
get output from your print statements even if the next statement
crashes your program.UNSCALED_POINTER_ADD
and UNSCALED_POINTER_SUB
. gprof
tool
helpful for optimizing performance. (man gprof
or
searching online for gprof
documentation will get
you the basics.) If you use gprof
, see the hint
about debugging above for how to pass extra arguments to GCC in
the Makefile
. This is an optional, but recommended, addition that will help you check to see if your allocator is doing what it should (or figure out what it's doing wrong if not). Dynamic memory allocators are notoriously tricky beasts to program correctly and efficiently. They are difficult to program correctly because they involve a lot of untyped pointer manipulation. In addition to the usual debugging techniques, you may find it helpful to write a heap checker that scans the heap and checks it for consistency.
Some examples of what a heap checker might check are:
Your heap checker will consist of the function int
mm_check(void)
in mm.c
. Feel free to rename
it, break it into several functions, and call it wherever you
want. It should check any invariants or consistency conditions you
consider prudent. It returns a nonzero value if and only if your
heap is consistent. This is not required, but may prove useful.
When you submit mm.c
, make sure to remove any calls
to mm_check
as they will slow down your throughput.
Others:
If you understand the concept well, I think it takes about 0.5-1 hour to finish the program for mm_malloc and mm_free, but it may take 1-3 hours to debug if you know how to debug efficiently. I would like to give the following advice:
1. If you are not familiar with malloc and free, read the implementation in implicit free list (in particular the mm_malloc and mm_free part) to know the structure of malloc and free:
http://www.cs.wm.edu/~liqun/teaching/cs304_15f/hws/malloc.html
2. Use examine_heap() to check your heap (DON"T SKIP THIS PART! I guarantee you preparing this or other helper function will save you enormous amount of time). The function of examine_heap() is very helpful. You may add more information in your own implementation of examine_heap() to print out the footer. You can check the free list as well. You can call examine_heap() before and after the code segment that you suspect would go wrong.
3. Create a short trace like the following to debug your mm_realloc(). You don't need to worry about the first line and fourth line. The second line and third line are the number of calls to malloc() and the number of operations (including all malloc, free and realloc).
20000
6
16
1
Hints: After seeing more implementations, here are some hints to help you.
0. Make sure you print out the footer information for your free block in examine_heap(). Very important. This way, you can check your footer is set correctly.
1. mm_malloc() returns WORD_SIZE added to the address of the header of the allocated block. And in mm_free(ptr), the header of the block you are going to free is (char*)ptr - WORD_SIZE.
2. Don't forget to change the TAG_PRECEDING_USED in the following block when you do mm_malloc and mm_free. Even if when you call mm_malloc in the first time, you still need to change the header of the following block.
3. When you need to get more memory, use reqestMoreSpace(), which gets more space than you ask. It will put a free block on the free list.
4. After you use searchFreeList, if you would like to allocate that block, you need to use removeFreeBlock to remove the block from the freelist.
5. After you get a block from freelist for malloc, you need to split the block to two (one is for your requested size, and another one is a new block) if the block is too big. The new block (after you prepare the header and footer) needs to be put back to freelist using insertFreeList().
6. mm_free(): you need to prepare the header and the footer for the block you will free, and change the TAG_PRECEDING_USED in the following block. Then call insertFreeBlock and coalesceFreeBlock().
7. First use the short traces to test your program:
./mdriver -f short1-bal.rep
./mdriver -f short2-bal.rep
8. When you malloc or free a block, change the TAG_PRECEDING_USED bit in the header of the following block. If the following block is free, you also need to change the footer of the following block.
mm.c
file: make submit