CSE logo University of Washington Computer Science & Engineering
 CSE 374 Programming Concepts and Tools - Homework 6 - Spring 2010
  CSE Home   About Us    Search    Contact Info 

 

Memory Management

Due in three parts:

Part 0: Pick a partner and send info by Saturday, May 15 at 11 pm

Part 1: Repository, header files, and function prototypes/skeletons by Thursday, May 20 at 11 pm
(NO LATE ASSIGNMENTS for this part)

Part 2: Final Code by Thursday, May 27 at 11 pm

Changes

5/19: A couple of bug fixes: Changed hw5.tar to hw6.tar in the Additional Requirement section. Clarified the wording on the turnin requirements for part 1 - the bench.c program needs to contain a main function, not a separate "main program".

Synopsis

In this assignment you will develop and benchmark a memory management package. You are required to work with a partner on this project. You and your partner should turn in a single assignment, and both partners will receive the same grade on the project. Be sure to have both of your names on the assignment, but only turn in a single copy under one of your names (and turn in the project under the same name for both parts, please). Also remember that if you wish to use a late day for the final part of the project, both you and your partner must have a late day available.

Step 0 - Pick a partner. Now!

As soon as possible, but no later than 11 pm on May 15, you should pick a partner. One of you (only!) should send a note to the instructor (perkins at cs), with your names and your UW netids. You must put 374 partners in the email subject heading, and please cc your partner so that a reply-all will reach both of you. We will use this information to assign you and your partner to group and set up a svn repository you can use for this assignment.

Assignment goals

This assignment continues our exploration of procedural programming, memory management, and software tools, as well as software development and working in groups. In particular, in this assignment you will:

Please get started early. Even though you are working with a partner, there is enough to do that you will be in (big) trouble if you wait to begin the weekend before it is due. To encourage this, you are required to turn in skeleton files for your code fairly early in the project (details at the end of this writeup).

Requirements

The project consists of two main technical pieces: a memory management package, and a program to exercise it and report statistics. The members of your group will be in charge of different parts of the assignment, as described below. Ultimately, however, both of you are responsible for, and should understand and be able to explain, all of the code.

Memory Management

The memory management package should include a header file mem.h and C implementation files that specify and implement the following four functions.

Dividing the Work

In a production system there would likely be a single implementation file containing all of the above functions. One person would be responsible for the implementation of that file, while the other person would test it. But for this class, we want to divide the work so that you and your partner both work on the details and use a svn repository to manage the shared files. Because of that, you should split your code into the following half-dozen files:

One person in your group should be the primary implementor in charge of getmem.c; the other person is in charge of freemem.c. Similarly, you should divide get_mem_stats.c and print_heap.c, with each of you taking responsibility for one of these files. You should share responsibility for the header files as needed. Each of you is responsible for testing the other's code.

Test and Benchmark

You should implement a program named bench, whose source code is stored in a file bench.c. When this program is run, it should execute a large number of calls to functions getmem and freemem to allocate and free blocks of random sizes and in random order. This program should allow the user to specify parameters that control the test. The command-line parameters, and their default values are given below. Trailing parameters can be omitted, in which case default values are used (square brackets [] mean optional, as is the usual convention for Unix commands).

Synopsis:   bench [ntrials [pctget [pctlarge [small_limit [large_limit [random_seed ]]]]]]

Parameters:

(The parameter list is, admittedly, complex, but the intent is that this program will be executed by various commands in your Makefile(s), so you will not have to repeatedly type long command lines to run it.)

When bench is executed, it should perform ntrials memory operations. On each operation, it should randomly decide either to allocate a block using getmem or free a previously acquired block using freemem. It should make this choice by picking a random number with a pctget chance of picking getmem instead of freemem. If the choice is to free a block and all previously allocated blocks have already been freed, then there is nothing to do, but this choice should be counted against the ntrials total and execution should continue.

If the choice is to free a block, one of the previously allocated blocks should be picked randomly to be freed. The bench program must pick this block and update any associated data structures used to keep track of allocated blocks in amortized constant (O(1)) time so that the implementation of the bench program does not have unpredictable effects on the processor time needed for the test.

The next three parameters are used to control the size of the blocks that are allocated. In typical use, memory managers receive many requests for small blocks of storage interspersed with some larger ones. To model this behavior, each time a new block is allocated, it should be a large block with probability pctlarge; otherwise it should be a small block (use a random number generator to make this decision with the requested probability). If the decision is to allocate a small block, request a block whose size is a random number between 1 and small_limit. If the decision is to allocate a large block, request a block whose size is is a random number between small_limit and large_limit.

While the test is running, the benchmark program should print the following statistics to stdout.

The program should print this 10 times during execution, evenly spaced during the test. In other words, the first report should appear after 10% of the total getmem/freemem calls have executed, then after 20%, 30%, etc., and finally after the entire test has run. You may format this information however you wish, but please keep it brief - one line for each set of output should be enough.

You and your partner should share responsibility for this program and file however you wish.

Additional Requirements

Besides the software specifications above, you should meet the following requirements for this assignment.

Repository Notes

You and your partner will be given a newly created Subversion repository on the machine cubist.cs.washington.edu. The full repository name to be used in svn commands is https://cubist.cs.washington.edu/repos/cse374_10sp_xx, where xx is a placeholder for your particular group's repository id. For example, to get a new working copy of a repository if you are in group qz, you would use the following svn command:

svn checkout https://cubist.cs.washington.edu/repos/cse374_10sp_qz

The first time you access a repository or create a working copy you will need to enter your password and possibly your user id. These are different accounts from your normal UW accounts and have a different password. If you are accessing the repository from an account that has a different user name than the uw netid that you gave us when we set up the group, you can specify that netid in the svn command, after which you will be prompted for the password. For example, if your netid is emery and you are creating a working directory on a machine using a different user name, you could access the repository with

svn checkout --username emery https://cubist.cs.washington.edu/repos/cse374_10sp_qz

Once you've created a working copy of a repository, the url, your user id, and your password are stored locally in the working copy and you shouldn't have to enter them again on subsequent svn commands.

Memory Management

The above section describes what you need to do. This section gives some ideas about how to do it. We will talk about this further in class, and you should take advantage of the online class discussion list to trade questions, ideas, and suggestions.

The basic idea behind the memory manager is fairly simple. At the core, the getmem and freemem functions share a single data structure, the free list, which is just a linked-list of free memory blocks that are available to satisfy memory allocation requests. Each block on the free list starts with an int that gives its size, and a pointer to the next block on the free list. To help keep data in dynamically allocated blocks properly aligned, we require that all of the blocks be a multiple of 8 bytes in size, and that their addresses also be a multiple of 8.

When a block is requested from getmem, it should scan the free list looking for a block of storage that is at least as large as the amount requested, delete that block from the free list, and return a pointer to it to the caller. When freemem is called, it should return the given block to the free list, combining it with any adjacent free blocks if possible to create a single, larger block instead of several smaller ones.

The actual implementation needs to be a bit more clever than this. In particular, if getmem finds a block on the free list that is substantially larger than the storage requested, it should divide that block and return a pointer to the portion that is large enough to satisfy the request, leaving the remainder on the free list. But if the block is only a little bit larger than the requested size, then it doesn't make sense to split it and leave a tiny chunk on the free list that is unlikely to be useful in satisfying future requests. You can experiment with this threshold and see what number is large enough to prevent excessive fragmentation, without wasting too much space that could have been used to satisfy small requests. The actual number should be a symbolic constant given in a #define in your code.

What if no block on the free list is large enough to satisfy a getmem request? In that case, getmem needs to acquire a good-sized block of storage from the underlying system, add it to the free list, then split it up, yielding a block that will satisfy the request, and leaving the remainder on the free list. Since requests to the underlying system are (normally) relatively expensive, they should yield a reasonably large chunk of storage, say at least 4K or 8K, that is likely to be useful in satisfying several future getmem requests. Normally the same amount is acquired each time it is necessary to go to the underlying system for more memory. But watch out for really big getmem requests. If getmem is asked for, say, a 20K block, it needs to get at least that much in a single chunk, since the underlying system cannot be relied on to return adjacent blocks of storage on successive calls.

So what is "the underlying system"? For our purposes, it is the standard malloc function! Your memory manager should acquire large blocks of storage from malloc when it needs to add blocks to its free list. malloc guarantees that the storage it returns is allocated on 8-byte, 16-byte, or larger boundaries on modern systems, so we don't need to worry about whether the block we get from malloc is properly aligned.

Notice that a request for a large block will happen the very first time getmem is called(!). When a program that uses getmem and freemem begins execution, the free list should be initially empty. The first time getmem is executed, it should discover that the (empty) free list does not contain a block large enough for the request, so it will have to call the underlying system to acquire some storage to work with.

What about freemem? When it is called, it is passed a pointer to a block of storage and it needs to add this storage to the free list, combining it with any immediately adjacent blocks that are already on the list. What freemem isn't told is how big the block is(!). In order for this to work, freemem somehow has to be able to find the size of the block. The usual way this is done is to have getmem actually allocate a block of memory that is a bit larger than the user's request, store the block size at the beginning of the block, and return a pointer that actually points a few bytes beyond the real start of the block. Then when freemem is called, it can take the pointer it is given, subtract the appropriate number of bytes to get the real start address of the block, and find the size of the block there.

How is freemem going to find nearby blocks and decide whether it can combine a newly freed block with one(s) adjacent to it? There are various ways to do this (as usual), but a good basic strategy is for getmem and freemem to keep the blocks on the free list sorted in order of ascending memory address. The block addresses plus the sizes stored in the blocks can be used to determine where a new block should be placed in the free list and whether it is, in fact, adjacent to another one.

It could happen that a request to freemem would result in one of the underlying blocks obtained from the system becoming totally free, making it possible to return that block to the system. But this is difficult to detect and not worth the trouble in normal use, so you shouldn't worry about this possibility.

Implementation Suggestions

Here are a few ideas that you might find useful. Feel free to use or ignore them as you wish.

The Benchmark Program

The command line can contain several integer parameters. These need to be converted from character strings ("500") to binary int values. There are various library functions that are useful: look at atoi and related ones. Take advantage of the Linux getopt library function if it helps.

The benchmark program relies heavily on random numbers. The standard library function rand can be used to generate sequences of pseudo-random numbers. Given a particular starting number (the seed), rand (or any pseudo-random number generator) will always generate the same sequence of numbers on successive calls. This can be very helpful during testing (i.e., things are basically random, but the sequence is reproducible). If you want to generate a different sequence of numbers each time the program is executed, you can set the seed to some quantity that is different on each run - the system time-of-day clock is a frequent choice, and should be the default if no seed is given on the benchmark program command line. Alternatively, modern Linux systems provide a special file /dev/urandom that returns random bytes whenever it is read, and you can read 4 bytes from here to get a random starting value.

One of the benchmark quantities that should be printed is the processor time used. The clock library function can be used to measure this. Store the time right before starting the tests, then subtract this beginning time from the current clock time whenever you need to get the elapsed time. Unfortunately, on many Linux systems clock is updated infrequently. If your test is fast enough that clock has the same value before and after the test, don't worry about it. Alternatively you can explore whethere there are better timing functions available. If you use one of these please be sure it is available on most Linux systems and not particular to only your distribution.

Finally, the benchmark program needs to keep track of all of the pointers returned by getmem but not yet freed, and randomly pick one of these to free when the "coin flip" says to free some storage. The obvious way to handle this is to allocate a "big enough" array using malloc (not using getmem! Why?) and store the pointers there. When a pointer is picked randomly to be freed, you can move another pointer from the end of the list to the spot occupied by the freed pointer and reduce the size of the list by 1. That way picking the pointer and updating the list can be done in O(1) (constant) time, so the order in which the pointers are picked won't affect the time needed by the benchmark program itself to run the tests.

Developing and Testing

As with all projects, you should start (very) small and incrementally build up the final project. Here are some ideas:

Extra Credit

Here are a couple of things you could add to your memory manager once it's working.

DO NOT ATTEMPT ANY OF THIS until you have completed the basic assignment and turned it in.

For more information, in addition to Google and Wikipedia, an authoritative discussion is in Sec. 2.5, Dynamic Storage Allocation, in The Art of Computer Programming, Vol. I: Fundamental Algorithms, by Donald Knuth. Doug Lea's web site (http://g.oswego.edu/dl/html/malloc.html) has good information about the allocator that he wrote that is part of many distributions.

What to Turn In (and When)

To help organize the project, and to stay on schedule, you should turn in this assignment in two phases.

Part 1: Header files and repository. 15% of the total credit for the entire assignment will be awarded for turning in a complete set of header files and skeleton implementations of everything required for the assignment, including the basic Makefile, and having these properly checked in to your svn repository. The header files should be essentially complete; the skeleton (stub) implementations (the .c files) can contain functions with either empty implementations or a dummy return statement as appropriate. These files should be complete enough to compile without errors when the files are unpacked in a directory on a standard Linux machine and a make command is executed there. The implementations may be more complete than this, but they only need to compile cleanly; they don't need to work yet. These files should also include a skeleton of the benchmark program with #includes of the necessary headers and has a skeleton main function (return 0; is perfectly fine). All files must contain appropriate comments, particularly heading comments on functions and interfaces, and information in each file to identify you and the project.

Your files should be stored in your code repository. However, you should create an archive containing the files and turn it in normally using the regular dropbox.

Part 2: Final code. This contains the complete project, including the README file and everything else requested above. Use the make dist target in your Makefile to create an archive containing all the requested files and turn that in.


CSE logo Computer Science & Engineering
University of Washington
Box 352350
Seattle, WA  98195-2350
(206) 543-1695 voice, (206) 543-2969 FAX
[comments to Hal Perkins]