Implement a Simple Memory Allocator
OpenAISoftware Engineer · OnsiteLast updated
This question evaluates low-level memory management and allocator design skills, including block header layout, free-list/data-structure choices, pointer arithmetic, fragmentation concepts, and analysis of time/space complexity.
Design and implement a simplified memory allocator exposing malloc(size) and free(ptr) over a single fixed-size heap (a contiguous byte array). The allocator must not request more memory from the operating system after initialization — it only manages the bytes it was given.
Walk from a straightforward correct implementation to an optimized one, and analyze the trade-offs.
Constraints & Assumptions
- The heap is one contiguous region, e.g.
byte[N], withNfixed at init. malloc(size)returns a pointer/offset to a block of at leastsizeusable bytes, or fails (returns null / sentinel) if no block can satisfy the request.free(ptr)marks a previously-allocated block reusable. You may assumeptrwas returned by a priormalloc(but discuss defending against misuse).- Single-threaded for the core problem; concurrency is a follow-up.
- Allocations should be aligned to a machine-friendly boundary (e.g. 8 or 16 bytes).
Clarifying Questions to Ask
- What is the heap size
N, and the expected allocation size distribution (many small vs. few large)? - What alignment guarantee must payloads satisfy?
- What is the contract for
malloc(0), double-free, and freeing an invalid pointer — defined behavior or undefined? - Are we optimizing for allocation latency, memory utilization, or worst-case fragmentation?
- Is the allocator single-threaded, or must it be thread-safe?
Part 1 — First-fit allocator with splitting
Implement malloc/free using a first-fit strategy: linearly scan the blocks (or a free list) and use the first free block large enough. When a chosen free block is larger than requested, split it so the remainder stays available. State the time/space complexity of each operation.
What This Part Should Cover
- A correct block model: header layout, the payload-vs-header pointer arithmetic, and how alignment is enforced.
- A working first-fit scan with a clear, justified
MIN_PAYLOAD/ minimum-split threshold. - Correct per-operation complexity for
malloc,free, andsplit, with reasoning rather than asserted constants.
Part 2 — Coalescing and fragmentation
Show how free should handle adjacent free blocks (the previous and next physical neighbors), and explain how this reduces external fragmentation. Distinguish external from internal fragmentation.
What This Part Should Cover
- A precise external-vs-internal fragmentation distinction (scattered gaps between blocks vs. slack inside a block).
- Two-sided coalescing with the previous and next physical neighbors, and why address-ordering makes the merge $O(1)$.
- An honest account of what coalescing does and does not buy — it limits, but cannot eliminate, external fragmentation.
Part 3 — Best-fit optimization
Propose an optimization that chooses the smallest free block that still fits the request, and pick a data structure that makes the lookup efficient. Justify the structure: explain why a plain min-heap keyed by size is not a clean fit, and what works instead.
What This Part Should Cover
- Names the query shape precisely (smallest free block with usable size $\geq$ request — a lower-bound / successor query, not a global minimum) and uses it to rule out a plain min-heap.
- Picks a structure whose operations match that query (ordered map / balanced BST keyed by size, or segregated size-class lists) and reasons about, rather than just names, the candidates.
- Recognizes that best-fit needs a second size-ordered index alongside the physical list, and states the consequence: every split and coalesce must keep both in sync.
Part 4 — Complexity & trade-off analysis
Compare first-fit and best-fit on allocation time, space/metadata overhead, and fragmentation behavior. State when each is preferable.
What This Part Should Cover
- A side-by-side comparison across allocation cost,
free/coalesce cost, metadata overhead, and long-run fragmentation behavior. - The key insight that minimizing leftover per allocation (best-fit) does not automatically minimize long-run fragmentation, and can manufacture tiny unusable slivers.
- A workload-aware verdict for latency vs. utilization vs. worst-case fragmentation, with no blanket "best-fit is better."
Hints
Hint 1
Block layout: Prefix each block's payload with a small **header** (`size`, `is_free`, and links to physical neighbors). `malloc` returns the *payload* pointer; recover the header on `free` by subtracting `header_size`. Keep blocks in **physical (address) order** so neighbors are reachable.
Hint 2
Splitting threshold: Don't split unconditionally. Think about what minimum the leftover must clear to be a usable block of its own — and what goes wrong if you split off something smaller than that.
Hint 3
Which neighbors, and why O(1): On `free`, you have two physical neighbors to consider. What property of the block ordering lets you reach both and merge in constant time? And keep the two fragmentation flavors straight: scattered free gaps vs. slack *inside* a block.
Hint 4
Frame the query precisely: Write down the exact query `malloc(size)` must answer over the free blocks. Is it "give me the global minimum," or something else? Once you've named the query shape, ask which structures answer *that* shape efficiently.
Hint 5
Production allocators: Real-world allocators don't keep a strict global ordering of every free block. Consider how grouping free blocks could approximate best-fit while staying close to constant time.
Hint 6
Which orderings does each operation need?: What ordering does coalescing rely on to reach a block's neighbors, and what ordering does the best-fit query rely on? Ask whether one structure can serve both queries efficiently — and if not, what that implies for every split and coalesce.
Hint 7
Dimensions to contrast: Line them up on allocation cost, `free` cost (does the structure need maintenance?), metadata size, and long-run fragmentation. Watch one subtle trap: "less wasted space per allocation" is not the same claim as "less long-run fragmentation."
Follow-up questions
- Make the allocator **thread-safe**. Start from a global lock, then reduce contention — per-thread caches, per-size-class locks, per-CPU arenas. Which operations must be atomic together?
- How would you implement **O(1) coalescing** with *boundary tags* (a footer replicating the header) so the previous physical block is reachable without a `prev` pointer?
- How do **segregated free lists** trade internal fragmentation for speed, and how do real allocators (e.g. buddy / slab systems) pick their size-class boundaries?
- How would you detect and defend against **heap corruption** (canaries, header checksums) without unacceptable overhead?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.