Overview
Today, we’ll use the problem set to explore alignment, dynamic allocation, invariants, tests, and internal metadata.
Problem set comment
After memory reuse, your m61_malloc might use a helper function like this:
static void* m61_find_free_space(size_t sz) { // do we have a freed allocation that will work? for (allocation& a : freed allocation set) { if (a is at least sz bytes big) { void* ptr = first byte in a; remove a from freed allocation set; return ptr; } } // otherwise fail return nullptr; }
Specifications
- A specification for a function is a high-level, but ideally precise, description of its desired behavior
 - Ideally a specification says what a function does, not how it does
it
- Allows optimizations
 
 - If a function implementation disagrees with its specification, that’s a bug
 - We often write specifications before function definitions in a comment
 
/// m61_malloc(sz, file, line)
///    Returns a pointer to `sz` bytes of freshly-allocated dynamic memory.
///    The memory is not initialized. If `sz == 0`, then m61_malloc may
///    return either `nullptr` or a pointer to a unique allocation.
///    The allocation request was made at source code location `file`:`line`.
- What is missing from this specification, if anything?
 - Write a specification for 
m61_find_free_space! 
m61_find_free_space specification
m61_mallocperforms an allocation, but does other work too- Maintains statistics
 - Checks for integer overflow
 - Rounds allocation size up to ensure alignment
 
m61_find_free_spacejust allocates space- Solves a smaller, more targeted problem
 - May simplify 
m61_malloc 
/// m61_find_free_space(sz)
///    Allocates a block of `sz` bytes and returns a pointer to that block.
///    Returns `nullptr` on failure. The newly-allocated block does not
///    overlap with any other concurrently-allocated block.
Handout code
void* m61_malloc(size_t sz, const char* file, int line) {
    (void) file, (void) line;   // avoid uninitialized variable warnings
    // Your code here.
    if (default_buffer.pos + sz > default_buffer.size) {
        // Not enough space left in default buffer for allocation
        return nullptr;
    }
    // Otherwise there is enough space; claim the next `sz` bytes
    void* ptr = &default_buffer.buffer[default_buffer.pos];
    default_buffer.pos += sz;
    return ptr;
}
- Can we refactor a version of 
m61_find_free_spaceout of this code? 
Rewritten m61_malloc
void* m61_find_free_space(size_t sz) {
    if (default_buffer.pos + sz <= default_buffer.sz) {
        void* ptr = &default_buffer.buffer[default_buffer.pos];
        default_buffer.pos += sz;
        return ptr;
    }
    return nullptr;
}
void* m61_malloc(size_t sz, const char* file, int line) {
    void* ptr = m61_find_free_space(sz);
    return ptr;
}
- How to add statistics collection?
 
Adding statistics
void* m61_find_free_space(size_t sz) {
    if (default_buffer.pos + sz <= default_buffer.sz) {
        void* ptr = &default_buffer.buffer[default_buffer.pos];
        default_buffer.pos += sz;
        return ptr;
    }
    return nullptr;
}
void* m61_malloc(size_t sz, const char* file, int line) {
    void* ptr = m61_find_free_space(sz);
    if (ptr) {
        global_statistics.ntotal += 1;
        global_statistics.total_size += sz;
        global_statistics.nactive += 1;
        global_statistics.active_size += sz;
    } else {
        global_statistics.nfail += 1;
        global_statistics.fail_size += sz;
    }
    return ptr;
}
- How to add alignment?
 
Alignment sidebar
Sizes of primitive types on x86-64
Type  | 
Size  | 
|---|---|
  | 
  1  | 
  | 
  2  | 
  | 
  4  | 
  | 
  8  | 
  | 
  8  | 
  | 
  4  | 
  | 
  8  | 
  | 
  16  | 
  | 
  8  | 
Standard sizes of primitive types
Type  | 
x86-64 Linux size  | 
Standard size  | 
|---|---|---|
  | 
  1  | 
  1  | 
  | 
  2  | 
  ≥1  | 
  | 
  4  | 
  ≥  | 
  | 
  8  | 
  ≥  | 
  | 
  8  | 
  ≥  | 
  | 
  4  | 
  ≥4 (probably)  | 
  | 
  8  | 
  ≥  | 
  | 
  16  | 
  ≥  | 
  | 
  8  | 
  N/A  | 
Alignment
- Hardware and compilers can impose restrictions on the addresses at which
certain types of object can appear
- The address of any 
intis a multiple of 4 (on x86-64) - The address of any pointer is a multiple of 8
 
 - The address of any 
 - This is called alignment
 - The C++ 
alignofoperator returns the alignment of a type or object- All objects of the same type have the same alignment
 alignof(std::max_align_t)is the maximum alignment for any type (16 on x86-64)
 
Alignments of primitive types
Type  | 
Alignment  | 
|---|---|
  | 
  1  | 
  | 
  2  | 
  | 
  4  | 
  | 
  8  | 
  | 
  8  | 
  | 
  4  | 
  | 
  8  | 
  | 
  16  | 
  | 
  8  | 
  | 
  16  | 
- Except for 
alignof(char), these values can change- On some x86-64 operating systems and compilers, 
alignof(long)= 4! - On x86-32 Linux, 
alignof(double)= 4 - On Apple Silicon, 
alignof(std::max_align_t)= 8 
 - On some x86-64 operating systems and compilers, 
 
Inductive alignment
- The alignment rule (from the problem set): “
malloc(sz)returns memory whose alignment works for any object.” - This means that every non-null pointer returned by 
m61_mallocmust have a numeric address value evenly divisible byalignof(std::max_align_t) - May require leaving little gaps between allocations
- Consider 
m61_malloc(1) - These gaps are called fragmentation
 
 - Consider 
 - The initial value of 
default_buffer.bufferis always correctly alignedassert(reinterpret_cast<uintptr_t>(default_buffer.buffer) % alignof(std::max_align_t) == 0)
 - Is there an easy way to ensure later allocations are also aligned?
 
Adding alignment
void* m61_find_free_space(size_t sz) {
    if (default_buffer.pos + sz <= default_buffer.sz) {
        void* ptr = &default_buffer.buffer[default_buffer.pos];
        default_buffer.pos += sz;
        return ptr;
    }
    return nullptr;
}
void* m61_malloc(size_t sz, const char* file, int line) {
    size_t alignment_space = /* YOUR CODE HERE */;
    size_t aligned_sz = sz + alignment_space;
    void* ptr = m61_find_free_space(aligned_sz);
    if (ptr) {
        global_statistics.ntotal += 1;
        global_statistics.total_size += sz;
        global_statistics.nactive += 1;
        global_statistics.active_size += sz;
    } else {
        global_statistics.nfail += 1;
        global_statistics.fail_size += sz;
    }
    return ptr;
}
Checking your arithmetic
- Computing 
alignment_spaceis a little tricky! - What are some facts that should always hold for 
alignment_space? - Why not add code to assert those facts?
 
Reusing memory
- Extend our 
m61_find_free_spaceto handle memory reuse (pseudocode) 
void* m61_find_free_space(size_t sz) {
    if (default_buffer.pos + sz <= default_buffer.sz) {
        // can perform allocation using default buffer
        void* ptr = &default_buffer.buffer[default_buffer.pos];
        default_buffer.pos += sz;
        return ptr;
    }
    // otherwise, try previously-freed allocations
    for (allocation& a : freed allocation set) {
        if (a is at least sz bytes big) {
            void* ptr = first byte in a;
            remove a from freed allocation set;
            return ptr;
        }
    }
    return nullptr;
}
Questions about allocation reuse
- How to represent 
freed allocation set? - How to populate 
freed allocation set? - How do these data structures relate?
- What invariants can we write to relate them?