Show all solutions
Test 1
Notes
Assume a Linux operating system running on the x86-64 architecture unless otherwise stated.
Some figures and code samples can be pinned to your browser so you can keep them visible as you scroll through the test. Push the 📌 and then drag the pinned figure where you want it.
The test questions concern Problem Set 1’s dynamic memory allocators and m61 tests that exercise its allocators. An m61 test is a complete program that indicates success by exiting with status 0, and indicates failure by failing an assertion, exiting with a nonzero status, or otherwise terminating abnormally.
1. Allocator testers (15 points)
QUESTION 1A. This tester aims to check that allocations are suitably aligned.
int main() {
void* p1 = m61_malloc(1);
void* p2 = m61_malloc(16);
assert((uintptr_t) p1 % 16 == 0);
assert((uintptr_t) p2 % 16 == 0);
}
Does this tester succeed or fail with a stub allocator whose m61_malloc
always returns nullptr? Explain briefly.
QUESTION 1B. Consider this tester.
int main() {
char* p = (char*) m61_malloc(4);
assert(&p[0] + 1 == &p[1] && &p[1] + 1 == &p[2] && &p[2] + 1 == &p[3]);
}
Which allocator behaviors does this tester actually check? List all that apply.
- That the allocation succeeded
- That the returned allocation is at least as large as requested
- That distinct allocations are disjoint
- That the returned allocation is suitably aligned
- None of the above
QUESTION 1C. Which of our four statistics is most difficult to support, and why?
total_counttotal_bytesactive_countactive_bytes- They’re all easy
QUESTION 1D. Consider m61-once.cc.
static char* buffer = nullptr;
static char* end_buffer = nullptr;
void* m61_malloc(size_t sz) {
if (!buffer) {
buffer = reinterpret_cast<char*>(malloc(8 << 20));
assert(buffer);
end_buffer = buffer + (8 << 20);
}
size_t space = end_buffer - buffer;
if (sz > space) {
// out of memory in buffer
return nullptr;
}
void* ptr = buffer;
buffer += sz;
return ptr;
}
void m61_free(void*) {
}
Which kind of test will almost certainly succeed for this allocator? List all that apply.
- Alignment tests
- Edge case tests (
m61_malloc(0),m61_free(nullptr)) - Disjoint allocation tests (allocated bytes have disjoint addresses)
- Reuse tests (freed memory can be reused)
- None of the above
4pt #3, disjoint allocation tests, is the one property this allocator gets right: the buffer advancement on line 20 ensures allocations don’t overlap, at least for nonzero sizes. Sizes are never rounded up, so only the first allocation is guaranteed aligned; repeated
m61_malloc(0)calls return the same address; andm61_freedoes nothing, so freed memory is never reused.
QUESTION 1E. Describe how to change m61-once.cc so that a call
m61_malloc(16000000) might succeed (return a valid non-null pointer). Be
specific.
3pt Line 8 becomes
buffer = reinterpret_cast<char*>(malloc(16 << 20));(or any number ≥ 16,000,000). Line 10 should change to use the same constant.
2. Two-faced allocation (15 points)
QUESTION 2A. The m61-lastalloc.cc
allocator
implements a limited form of memory reuse: when an application frees its most
recent allocation, that memory is reused. “I can do better,” said two-faced
god Janus. This is the body of m61-janus.cc. You may assume it has no
hidden bugs.
static char* buffer = reinterpret_cast<char*>(malloc(8 << 20));
static char* end_buffer = buffer + (8 << 20);
static char* first_buffer = buffer;
static char* last_buffer = buffer;
static std::map<char*, size_t> alloc_sizes;
void* m61_malloc(size_t sz) {
if (sz == 0 || (sz % 16 != 0 && sz <= (8 << 20))) {
sz += 16 - (sz % 16); // ensure alignment & unique allocations
}
char* cptr;
if (static_cast<size_t>(first_buffer - buffer) >= sz) {
first_buffer = first_buffer - sz;
cptr = first_buffer;
} else if (static_cast<size_t>(end_buffer - last_buffer) >= sz) {
cptr = last_buffer;
last_buffer = last_buffer + sz;
} else {
return nullptr;
}
alloc_sizes.insert({ cptr, sz });
return cptr;
}
void m61_free(void* ptr) {
if (ptr) {
char* cptr = reinterpret_cast<char*>(ptr);
auto it = alloc_sizes.find(cptr);
assert(it != alloc_sizes.end());
size_t sz = it->second; // extract size from <cptr, size> pair
alloc_sizes.erase(it);
if (cptr == first_buffer) {
first_buffer = cptr + sz;
} else if (cptr + sz == last_buffer) {
last_buffer = cptr;
}
}
}
Which line of m61-janus.cc detects invalid frees? Explain briefly.
3pt Line 30,
assert(it != alloc_sizes.end()). It asserts that the supplied pointer is in thealloc_sizesmap, which only happens if it’s an active allocation.
QUESTION 2B. Which regions of the m61-janus.cc buffer does m61_malloc
treat as free space? Refer to specific variables.
2pt The area between
bufferandfirst_bufferis free, and so is the area betweenlast_bufferandend_buffer.
QUESTION 2C. Swapping lines 17 and 18 of m61-janus.cc creates a subtle bug:
last_buffer = last_buffer + sz; // WRONG ORDER!
cptr = last_buffer;
What kind of tests might detect this bug, but would not fire for the original
m61-janus.cc? Explain briefly.
- Alignment tests
- Edge case tests (
m61_malloc(0),m61_free(nullptr)) - Disjoint allocation tests (allocated bytes have disjoint addresses)
- Reuse tests (freed memory can be reused)
- None of the above
QUESTION 2D. Write or describe a reuse test that succeeds for
m61-janus.cc, but fails for the other “hacky” reusing allocators we’ve
discussed
(m61-lastalloc.cc,
which can reuse the most recent allocation, and
m61-reuseempty.cc,
which marks the whole buffer as free when the active_count statistic drops
to 0).
6pt The key intuition here is that
m61-januscan reuse memory indefinitely as long as each freed allocation is either the smallest allocation or the largest allocation in address terms. Many tests could be built on that intuition; here’s one:int main() { void* ptr1 = m61_malloc(5 << 20); // 5 MiB void* ptr2 = m61_malloc(11 << 18); // ~2.75 MiB assert(ptr1 && ptr2); m61_free(ptr1); void* ptr3 = m61_malloc(2 << 20); assert(ptr3); }
QUESTION 2E. (Extra credit) Write invariant assertions that relate the values
of m61-janus.cc’s first_buffer and last_buffer variables and its
alloc_sizes variable.
3pt extra credit.
buffer <= first_buffer && first_buffer <= last_buffer && last_buffer <= end_bufferalloc_sizes.empty() || alloc_sizes.begin()->first >= first_bufferalloc_sizes.empty() || std::prev(alloc_sizes.end())->first + std::prev(alloc_sizes.end())->second <= last_buffer