Implement a Simple Memory Allocator

Quick Overview

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.

Implement a Simple Memory Allocator

Company: OpenAI

Role: Software Engineer

Category: Software Engineering Fundamentals

Difficulty: medium

Interview Round: Onsite

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]`, with `N` fixed at init. - `malloc(size)` returns a pointer/offset to a block of **at least** `size` usable bytes, or fails (returns null / sentinel) if no block can satisfy the request. - `free(ptr)` marks a previously-allocated block reusable. You may assume `ptr` was returned by a prior `malloc` (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. ```hint 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 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. ``` #### 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`, and `split`, 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. ```hint 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. ``` #### 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. ```hint 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 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 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. ``` #### 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. ```hint 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." ``` #### 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." ### What a Strong Answer Covers These cut across all four parts — an interviewer tracks them regardless of which part is under discussion: - One consistent set of conventions held throughout: a single `size` semantics (total vs. payload), one alignment rule, one source of pointer arithmetic — so the parts build on each other without contradiction. - Edge cases handled coherently end-to-end: `malloc(0)`, out-of-memory, minimum block size, double-free / invalid-pointer detection, and never returning a pointer that overlaps allocator metadata. - Correct complexity for **every** operation under each strategy, with the cost of maintaining any auxiliary index attributed to the right operation. - Engineering judgment over recipe-following: trade-offs framed against the workload rather than asserting a single "right" answer. ### 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?

Quick Answer: 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.

|Home/Software Engineering Fundamentals/OpenAI
OpenAI logo
OpenAI
May 29, 2026, 12:00 AM
mediumSoftware EngineerOnsiteSoftware Engineering Fundamentals
33
0

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] , with N fixed at init.
  • malloc(size) returns a pointer/offset to a block of at least size usable bytes, or fails (returns null / sentinel) if no block can satisfy the request.
  • free(ptr) marks a previously-allocated block reusable. You may assume ptr was returned by a prior malloc (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 Guidance

  • 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 Guidance

  • 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 , and split , 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 Guidance

  • 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)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 Guidance

  • 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 Guidance

  • 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."

What a Strong Answer Covers Guidance

These cut across all four parts — an interviewer tracks them regardless of which part is under discussion:

  • One consistent set of conventions held throughout: a single size semantics (total vs. payload), one alignment rule, one source of pointer arithmetic — so the parts build on each other without contradiction.
  • Edge cases handled coherently end-to-end: malloc(0) , out-of-memory, minimum block size, double-free / invalid-pointer detection, and never returning a pointer that overlaps allocator metadata.
  • Correct complexity for every operation under each strategy, with the cost of maintaining any auxiliary index attributed to the right operation.
  • Engineering judgment over recipe-following: trade-offs framed against the workload rather than asserting a single "right" answer.

Follow-up Questions Guidance

  • 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?
Loading comments...