Description
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 will turn in a single assignment, and both partners will receive the same grade on the project. Be sure to have both of your names in the assignment files. Also remember that if you wish to use a late day or two for the final part of the project, both you and your partner must have those late days available, and both of you will be charged for any late days used.
Part 0: Pick a partner NOW!
As soon as possible, but no later than 11:00 pm on Monday, May 7, you must pick a partner and notify us. One of you (only!) must completeΒ this Google formΒ by writing in the names and uwnetids of both partners. We will create a discussion on the Canvas discussion board for students to find partners whose schedule might be compatible.
You must work in PAIRS. You cannot work alone and you cannot have a group of three or more.
We will use this information to set up aΒ gitΒ repository for your group on the CSE GitLab server. You must use this repository for this assignment; you cannot use another repository elsewhere. (And, as is true of all assignments, your solution code should not be publicly available on any repository where it could be accessed by other students in the class this quarter or in the future.)
You must work with a partner on this assignment; you cannot work alone. Part of the point of the assignment is to gain experience handling source code when more than one person is working on a project. If you do not have a partner by the deadline, you will be randomly assigned a partner for the assignment.
You and your partner will receive 1 point (1%) of the total credit for the assignment if you follow these instructionsΒ exactlyΒ – exactly one web form for the group filled out on time with the right information (names and uwnetids, not student id numbers, random email addresses, or other information).
Part 1: Header files and repository (due 5/15/18, 11pm)
SeeΒ the turnin sectionΒ for details about what is required for Part 1. Turnin will be through GitLab. NO LATE SUBMISSIONS.
Part 2: Final code (due 5/24/18, 11pm)
SeeΒ the turnin sectionΒ for final turnin instructions through GitLab.
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:
- Implement and test a memory management package that has the same functionality as the standard libraryΒ
mallocΒ andΒfreeΒ functions, - Gain experience using source-code management systems, in particular git,
- Gain further experience with software development tools likeΒ
make, and - Gain experience working in groups.
PleaseΒ start now. Even though you are working with a partner, there is enough to do that you will be inΒ (big)Β trouble if you wait until the weekend before it is due to begin. To encourage you to get started now you are required to turn in skeleton files for your code fairly early in the project (details later in 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 submitted by your group.
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.
| Function | Description |
|---|---|
void* getmem(uintptr_t size) |
Return a pointer to a new block of storage with at leastΒ sizeΒ bytes of memory. The pointer to the returned block should be aligned on an 16-byte boundary (i.e., its address should be a multiple of 16). The block may be somewhat larger than the size requested, if that is convenient for the memory allocation routines, but should not be significantly larger, which would waste space. The valueΒ sizeΒ must be greater than 0. IfΒ sizeΒ is not positive, or if for some reasonΒ getmemΒ cannot satisfy the request, it should returnΒ NULL. TypeΒ uintptr_tΒ is an unsigned integer type that can hold a pointer value (i.e., can be converted to or from a pointer of typeΒ void *, or other pointer type, exactly). It is defined in headerΒ <inttypes.h>Β (and alsoΒ <stdint.h>). See discussion below. |
void freemem(void* p) |
Return the block of storage at locationΒ pΒ to the pool of available free storage. The pointer valueΒ pΒ must be one that was obtained as the result of a call toΒ getmem. IfΒ pΒ isΒ NULL, then the call toΒ freememΒ has no effect and returns immediately. IfΒ pΒ has some value other than one returned byΒ getmem, or if the block it points to has previously been released by another call toΒ freemem, then the operation ofΒ freememΒ is undefined (i.e.,Β freememΒ may behave in any manner it chooses, possibly causing the program to crash either immediately or later; it is under no obligation to detect or report such errors).An additional implementation requirement: WhenΒ freememΒ returns a block of storage to the pool, if the block is physically located in memory adjacent to one or more other free blocks, then the free blocks involved should be combined into a single larger block, rather than adding the small blocks to the free list individually. |
void get_mem_stats( |
Store statistics about the current state of the memory manager in the three integer variables whose addresses are given as arguments. The information stored should be as follows:
See the discussion below outlining the implementation of the memory manager for more details about these quantities. |
void print_heap(FILE * f) |
Print a formatted listing on fileΒ fΒ showing the blocks on the free list. Each line of output should describe one free block and begin with two hexadecimal numbers (0xdddddddd, whereΒ dΒ is a hexadecimal digit) giving the address and length of that block. You may include any additional information you wish on the line describing the free block, but each free block should be described on a single output line that begins with the block’s address and length. |
In addition, there should be a separate header fileΒ mem_impl.hΒ and C implementation file for the following function, which is used internally in the memory manager implementation, but is not intended to be used by client code.
| Function | Description |
|---|---|
void check_heap() |
Check for possible problems with the free list data structure. When called this function should useΒ asserts to verify that the free list has the following properties:
If no errors are detected, this function should return silently after performing these tests. If an error is detected, then anΒ |
Dividing the Work
In a production memory manager there would likely be a singleΒ .cΒ 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Β GitLabΒ repository to manage the shared files. Because of that, you should split your code into the following set of files:
| File | Description |
|---|---|
mem.h |
Header file containing declarations of the public functions in the memory manager (including appropriate comments). This is the interface that clients of yourΒ getmem/freememΒ package would use. |
getmem.c |
Implementation of functionΒ getmem. |
freemem.c |
Implementation of functionΒ freemem. |
get_mem_stats.c |
Implementation of functionΒ get_mem_stats. |
print_heap.c |
Implementation of functionΒ print_heap. |
mem_utils.c |
Implementation of functionΒ check_heap. This is also a good place to put any other shared code or functions that are used internally by other parts of the implementation but are not intended to be part of the public interface. |
mem_impl.h |
Header file with declarations of internal implementation details shared by more than one of the above files. This is information required in more than one of the implementation files, but that is not part of the public interface, which is declared in fileΒ mem.h. In particular, this is where the declarations of the free list data structure(s) should reside, as well as the declaration of functionΒ check_heap. |
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,Β print_heap.c, andΒ mem_utils.cΒ with each of you taking responsibility for one or two 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 should be used. Square bracketsΒ []Β mean optional, as is the usual convention for Linux command descriptions.
Synopsis: Β Β bench [ntrials] [pctget] [pctlarge] [small_limit] [large_limit] [random_seed]
Parameters:
ntrials: total number ofΒgetmemΒ plusΒfreememΒ calls to randomly perform during this test. Default 10000.pctget: percent of the totalΒgetmem/freememΒ calls that should beΒgetmem. Default 50.pctlarge: percent of theΒgetmemΒ calls that should request “large” blocks with a size greater thanΒsmall_limit. Default 10.small_limit: largest size in bytes of a “small” block. Default 200.large_limit: largest size in bytes of a “large” block. Default 20000.random_seed: initial seed value for the random number generator. Default: some more-or-less random number such as the the system time-of-day clock (or bytes read fromΒ/dev/urandomΒ if you’re feeling adventurous).
(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 allocate a block, then, if the pointer returned byΒ getmemΒ is notΒ NULL, theΒ benchΒ program should store the valueΒ 0xFEΒ in each of the first 16 bytes of the allocated block starting at the pointer address returned byΒ getmem. If the requested block size is smaller than 16 bytes, all of the requested bytes should be initialized toΒ 0xFE.
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 more requests for small blocks of storage than large ones, and the order of requests is often unpredictable. 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 specified 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:
- Total CPU time used by the benchmark test so far in seconds (show enough fractional digits to provide useful information if possible, although the granularity of the system clock may be too large for this to be meaningful for short tests).
- Total amount of storage acquired from the underlying system by the memory manager during the test so far (e.g., theΒ
total_sizeΒ quantity fromΒget_mem_stats, above). - Total number of blocks on the free storage list at this point in the test.
- Average number of bytes in the free storage blocks at this point in the test.
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 and understandable – one line for each set of output numbers should be enough.
Once your code is working without problems, you might want to rerunΒ benchΒ after recompiling the code withΒ -DNDEBUGΒ to turn off theΒ assertΒ tests inΒ check_heapΒ to see how much faster the code runs without them. However, leave theΒ check_heapΒ tests on while developing and debugging your code since this will be a big help in catching errors.
You and your partner should share responsibility for this program and file however you wish.
Additional Requirements
Besides the software specifications above, you must meet the following requirements for this assignment.
- You and your partner must use a CSE GitLab repository to store all of the code and other files associated with the project. (But don’t store things likeΒ
.oΒ files and executable programs that don’t belong in a repository.) You must use the repository that we provide even if you have separate machines or accounts of your own that you use for other projects. Both you and your partner should be regularly committing and pushing changes to your repository and we expect the git log to reflect reasonable activity by both members of the group. Don’t obsess about the number of commits/pushes done by each person, however the git log must show commit activity by both partners for both parts of the project. - You should create aΒ
MakefileΒ with at least the following targets:benchΒ (this should be the default target). Generate theΒbenchΒ executable program.test. Run theΒbenchΒ test program with default parameters. This should recompile the program first if needed to bring it up to date.clean. Remove anyΒ.oΒ files, executable, emacs backup files (*~), and any other files generated as part of making the program, leaving only the original source files and any other files in the directory unrelated to the project.
You may add any additional targets that you wish for your convenience. There are some ideas for useful targets in the Implementation Suggestions section, below.
- You should create aΒ
READMEΒ file at the top level of your repository. This file should give a brief summary of:- Both of your names, and your group identifier (2 letters).
- Who was responsible for which part of the project, and how the work was divided.
- A brief description of how your heap (free list) data structure is organized and the algorithms used to manage it.
- A summary of any additional features or improvements in your memory manager or benchmark code. If you did any extra credit parts of the assignment, be sure to describe that. If you experimented with various quantities such as the minimum size of a block fragment to keep on the free list, describe your experiments and results obtained.
- A summary of the results you observed on several runs of yourΒ
benchΒ program. This does not need to be exhaustive (and should not be exhausting), but it should give the reader an idea of how your code worked, how fast it was, and how efficient it was in its use of memory. - A summary of any resources you consulted for information about memory management algorithms. Your code, of course, must be your own, but feel free to research and explore memory management topics.
- Finally, your code should be of the usual high quality, with clean layout, good comments, and so forth. In particular, the comments describing the free list data structures should contain a complete but succinct description of this data so that someone can read these definitions and understand them without tracing the code that uses them. UseΒ
clintΒ to check for possible style issues that may need correcting.
Repository Notes
You and your partner will be given a newly createdΒ gitΒ repository hosted on the CSE department’s GitLab server (https://gitlab.cs.washington.edu). To get a new working copy of the repository if you are in groupΒ xy, you should use the followingΒ gitΒ command:
git clone git@gitlab.cs.washington.edu:cse374-18sp-students/cse374-18sp-xy.git
You will need to log on to GitLab and create an appropriate ssh key for this command to work (and if it asks for a password, you need to go back and fix the ssh key or create a new one –Β gitΒ should not ask for a password if everything is set up properly).
See the course website for links to a CSE 374 GitLab Tutorial and other reference information.Β Caution:Β If you have trouble gettingΒ git/GitLab to work properly, please use office hours, the discussion board, or email to the course staff to sort things out promptly. Web searches forΒ gitΒ hints are particularly likely to lead you seriously astray, suggesting all sorts of things that not only are not useful, but could leave your repository in a strange, possibly seriously damaged state that will be hard to unscramble.
Memory Management
The above sections describe what you need to do. This section gives some ideas about how to do it. We discuss 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Β uintptr_tΒ integer that gives its size followed by 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 16 bytes in size, and that their addresses also be a multiple of 16.
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 a 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 by 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 or more, 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 200K block, and no block currently on the free list is that large, it needs to get at least that much in a single request 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, we’ll use 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Β normally guarantees that the storage it returns is aligned on 16-byte or larger boundaries on modern systems, so we won’t 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 called, 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. If implemented cleanly, this will not be an additional “special case” in the code — it’s just the normal action taken byΒ getmemΒ when it needs to get new blocks for the free list!
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 to the caller a pointer to the storage that the caller can use, but which 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 (i.e., fromΒ malloc) 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 deal with this possibility in your code.
Implementation Suggestions
Here are a few ideas that you might find useful. Feel free to use or ignore them as you wish, although you do need to use the 64-bit pointer types correctly.
64-bit Pointers and ints
Your code should work on, and we will evaluate it on, the CSE Linux systems (klaatuΒ and the CSE virtual machine). These are 64-bit machines, which means pointers and addresses are 64-bit (8-byte) quantities. Your code will probably work on other 64-bit machines, and, if you’re careful, might work on 32-bit machines if it is recompiled, although we won’t test that.
One thing that is needed in several places is to treat pointer values as unsigned integers so we can do arithmetic to compute memory block addresses and sizes. We need to be able to cast 64-bit values between integer and pointer types without losing any information. Fortunately the libraryΒ <inttypes.h>Β contains a number of types and macros that make the job easier (and fairly portable!). The main type we want to use isΒ uintptr_t, which is a type that is guaranteed to be the right size to hold a pointer value so that we can treat it as an unsigned integer. A pointer value (void*Β or any other pointer type) can be cast toΒ uintptr_tΒ to create an integer value for arithmetic, andΒ uintptr_tΒ values can be cast to pointers when they hold integers that we want to treat as addresses. (There is also anΒ intptr_tΒ type that is a signed integer type of the right size to hold a pointer, but for our project it would be best to stick with unsigned values.)
You can print pointers andΒ uintptr_tΒ values withΒ printf. Use formatΒ %pΒ to print a pointer value, e.g.,Β printf("%p\n", ptr);. ForΒ uintptr_tΒ values, since these are stored as long, unsigned integers on our 64-bit systems, they can be printed as decimal numbers using theΒ %luΒ format specifier:Β printf("%lu\n",uintvalue);. It turns out thatΒ <inttypes.h>Β defines string macros that make it possible to print values without knowing the actual size of the underlying type. The magic incantation to print anΒ uintptr_tΒ valueΒ uiΒ isΒ printf("%" PRIuPTR "\n", ui);. There are other formatting macros to do things like print signed integer pointer values as decimal numbers (PRIdPTR) or in hex (PRIxPTR). See a good C reference for details.
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 a different value for each execution 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 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 whether there are better timing functions available. If you use one of these please be sure it is available on the CSE Linux machines so the program will work when we run it. (This has been a problem in the past when people developed the code using other systems only to have their entire project fail to compile because they were using a timing function or header that was not portable and not found on the CSE machines.)
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 toss” 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:
- In the past, many successful teams have found that implementingΒ
benchΒ first and then tacklingΒgetmemΒ andΒfreememΒ has been a good strategy. You can use stub versions of the memory manager functions to getΒbenchΒ working, and then it is available to help test the memory manager routines as you work on them. You should definitely consider doing this. - You can create initial versions ofΒ
getmemΒ andΒfreememΒ by implementing them as calls toΒmallocΒ andΒfree(!). That will allow work on the benchmark program to proceed independently ofΒgetmemΒ andΒfreemem. Plus if there is a problem later in the project, you can always substitute these stub versions to see if the trouble is inΒgetmem/freememΒ or in the benchmark program. - You can implementΒ
getmemΒ first by itself. Just haveΒfreememΒ return without doing anything. GetΒfreememΒ working later. - Use small tests involving very fewΒ
getmem/freememΒ requests when you are first testing the memory manager routines. - TheΒ
print_heapΒ function can be very helpful during debugging. Get it working early. Also,ΒgdbΒ can be very useful for exploring the free list (expeciallyΒgdb'sΒxΒ command) and for examining the operation of your code. - Write several small test programs whose effect on the heap you can predict by hand, then use the free list printout (above) and/orΒ
gdbΒ to check that it really works as you expect. - Don’t be shy about adding lots of targets to yourΒ
MakefileΒ to compile and run small test programs, or run the benchmark program with various argument values. If you find yourself typing the same command more than a few times to run a test, add it to yourΒMakefileΒ as the command for a target with a suitable name (e.g.,Βtest17,Βtest42,Βreallybigtest, etc.). - TheΒ
get_mem_statsΒ function may be useful during debugging to see the effect on the free list of various patterns ofΒgetmemΒ andΒfreememΒ requests. Don’t feel constrained to use it only to produce the required benchmark program reports. - UseΒ
check_heap()Β and otherΒasserts in your program. These can be particularly useful while you are testing and debugging, especially to check that pointers are notΒNULLΒ when they shouldn’t be and that the heap data structures have not been corrupted. In particular, include calls toΒcheck_heap()Β at the beginning and end ofΒgetmemΒ andΒfreememΒ to verify that those functions don’t introduce any obvious, checkable errors in the free list. Leave theΒasserts andΒcheck_heap()Β calls in your code even after things seem to be working. You can always putΒ-DNDEBUGΒ in aΒgccΒ command in someΒMakefileΒ target to disable asserts if you want to run your code without them. - Note thatΒ
valgrindΒ is unlikely to be particularly helpful for this assignment. We are manipulating pointers in non-standard ways andΒvalgrindΒ will probably report many spurious problems that are not really errors given what the code needs to do. - Be sure to commit and push code to your Gitlab repository regularly. That ensures that you have backup copies of your files, and also makes it easy (or possible!) to revert to previous versions of the code if needed.
Extra Credit
Here are a couple of things you could add to your memory manager once it’s working.
- (easy) IfΒ
getmemΒ always starts scanning the free list from the beginning when it is looking for a block of suitable size, it is likely that eventually there will be lots of little fragments of free space at the beginning of the list. We can reduce fragmentation, and speed things up, if each subsequent search starts from where the previous search left off, wrapping around to the front of the free list if the end is reached before finding a suitable block. How does the output of your benchmark program change if you do this? - (harder) Modify the free list and memory allocation routines so that blocks can be added to the free list and combined with adjacent blocks in constant time. One way to do this is the following, known as theΒ boundary tagΒ method. In addition to the header information at the beginning of each block containing its size, every block, both allocated and on the free list, should contain an extra few bytes at the end with length information and/or extra pointers and/or “free/allocated” bits. The idea is that when a block is being freed, we can look at the adjacent storage in the heap to find the end and beginning of the previous and next blocks, and from there we can determine whether they are free or allocated and how big they are without having to search the free list.
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 (https://g.oswego.edu/dl/html/malloc.html) has good information about the allocator that he wrote that was basis of theΒ malloc/freeΒ implementations in many C distributions.
What to Turn In
For this assignment, you will “turn in” the project by committing and pushing files to your group’s GitLab repository, then pushing a git “tag” to indicate which version of the files in your repository are the ones you wish us to grade for each part. 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.Β 14% of the total credit for the entire assignment will be awarded for having a complete set of header files and skeleton implementations of everything required for the assignment, including the basicΒ Makefile, properly committed and pushed to your GitLab 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 NULLΒ orΒ return 0Β statement if needed. These files should be complete enough to compile without errors when your repository is copied to a directory onΒ klaatuΒ or the 64-bit CSE Linux VM and aΒ makeΒ command is executed there after checking out the properΒ hw6-part1Β git tag (see details below). The implementations may be more complete than this, but they only need to compile cleanly at this point; nothing needs to work yet. These files should also include a skeleton of the benchmark program that hasΒ #includesΒ for the necessary headers and contains a skeleton main function (return EXIT_SUCCESSΒ is perfectly fine). All files must contain appropriate comments, particularly heading comments on functions and interfaces, and information in each file to identify your team and the project.
For part 1, the git log will probably have fairly little activity, but it must show at least one commit operation done by each partner in the group. That is to ensure that both people in the group have a properΒ gitΒ setup, and have been able to clone the repository, make changes, and commit and push those change to GitLab.
Part 2: Final code.Β This contains the complete project, including theΒ READMEΒ file and everything else requested above. Your files need to be committed and pushed to your repository, and marked with aΒ hw6-finalΒ tag. If you do any of the extra credit parts, be sure to commit and push the basic project using theΒ hw6-finalΒ tag, then, after you have committed and push the extra credit parts, mark those by pushing aΒ hw6-extraΒ tag to indicate those files.
“Turning In” hw6
As indicated above, submitting this assignment basically means having the final files pushed to your group’s GitLab repository. Once you’re ready, “turning in” the assignment is simple — create an appropriate tag in your git repository to designate theΒ gitΒ revision (commit) that the course staff should examine for grading. But there are multiple ways to get this wrong, so you shouldΒ carefullyΒ follow the following stepsΒ in this order. The idea is:
- Tidy up and be sure that everything is properly committedΒ and pushedΒ to your GitLab repository.
- Add a tag to your repository to specify the commit that corresponds to the finished assignment,Β afterΒ you have pushed all of your files.
- Check out a fresh copy of the repository and verify that everything has been done properly.
1. Tidy up and be sure everything is properly stored in Gitlab. Commit and push all of your changes to your repository (see the course web pages for links toΒ gitΒ information if you need a refresher on how to do this). Then in the top-level repository directory (i.e., inΒ cse374-18sp-xy, whereΒ xyΒ is your group’s code) do this:
bash% git pull bash% make clean bash$ git status On branch master Your branch is up-to-date with 'origin/master'. nothing to commit, working directory clean
If you see any messages about uncommitted changes or any other indications that the latest version of your code has not been pushed to the GitLab repository, fix those problems and push any unsaved changes before going on. Then repeat the above steps to verify that all is well.
2. Tag your repository and push the tag information to GitLab to indicate that the current commit is the version of the assignment that you are submitting for grading. For part 1, this would be:
bash% git tag hw6-part1 bash% git push --tags
Do notΒ do this untilΒ afterΒ you have committedΒ and pushedΒ all parts of your hw6 part 1 solution to GitLab.
You will do the same thing for the second part of the assignment, only using the tagΒ hw6-finalΒ instead ofΒ hw6-part1.
3. Check your work! Verify that everything is properly stored and tagged in your repository. To be sure that youΒ reallyΒ have updated and tagged everything properly, create aΒ brand new,Β emptyΒ directory that isΒ nowhere nearΒ your regular working directory, clone the repository into the new location, and verify that everything works as expected. It is really,Β really,Β REALLYΒ important that this not be nested anywhere inside your regular, working repository directory. Do this:
bash% cd <somewhere-completely-different> bash% git clone git@gitlab.cs.washington.edu:cse374-18sp-students/cse374-18sp-xy.git bash% cd cse374-18sp-xy bash% git checkout hw6-part1 bash% ls ...
Use your group’s 2-letter code instead ofΒ xy, of course. The commands afterΒ git cloneΒ change to the newly cloned directory, then cause git to switch to the tagged commit you created in step 2, above. We will do the same when we examine your files for grading.
At this point you should see your hw6 part 1 files. RunΒ make, then run any tests that you want. If there are any problems,Β immediatelyΒ erase this newly cloned copy of your repository (rm -rf cse374-18sp-xy), go back to the regular repository copy where you’ve been doing your work, and fix whatever is wrong. It may be as simple as running a missedΒ git push --tagsΒ command if the tag was not found in the repository. If it requires more substantive changes, you may need to do a little voodoo to get rid of the originalΒ hw6-part1Β tag from your repository and re-tag after making, committing, and pushing your repairs. To eliminate theΒ hw6-part1Β tag, do this (this should not normally be necessary):
bash% git tag -d hw6-part1 bash% git push origin :refs/tags/hw6-part1
Once you have made your repairs, andΒ onlyΒ after all the changes are committed and pushed, repeat the tag and tag push commands from step 2.Β And then repeat this verification step to be sure that the updated version is actually correct.
Once again: if you discover that repairs are needed when you check your work, it isΒ crucialΒ that you delete the newly cloned copy and make the repairs back in your regular working repository. If you modify files in the cloned copy you may wind up pushing changes to GitLab that leave your repository in a strange state, and files may appear to mysteriously vanish. Please follow the instructionsΒ precisely.
Follow the same instructions to verify your work after you’ve finished part 2 of the assignment, but using the tagΒ hw6-finalΒ instead ofΒ hw6-part1.

