Contiguous Memory Allocation
Imagine an operating system managing a computer with 1 GB of RAM. A rapid succession of processes is starting and finishing. Each process needs a single unbroken block of memory to run in. The OS must decide where to put it and how much to give it. It must also decide what to do when the process leaves. This is the essence of contiguous memory allocation. Every process occupies one continuous range of physical addresses, from a base address to that base plus its size. This problem matters because it is the ancestor of nearly every memory-management technique used in real systems. Before paging and segmentation existed, operating systems allocated memory exactly this way. Early mainframe batch systems and simple embedded kernels used one contiguous slab per process. Even today, contiguous allocation ideas resurface inside kernel allocators and buffer-pool managers. They also appear in embedded RTOS memory managers and GPU memory allocators. This happens anywhere a request must be satisfied by one physically or logically continuous region. The big picture is a trade-off between simplicity and waste. Contiguous allocation is easy to implement and gives fast address translation. A single base register (with Address Spaces and Relocation) suffices to relocate every access. But blocks must be whole and unbroken. Because of this, memory tends to shatter into unusable slivers — a phenomenon called fragmentation. The placement strategy the OS chooses to combat this has real, measurable performance consequences. This article builds the model from the ground up. It covers fixed partitioning versus variable partitioning. It also covers the three classic placement algorithms: first-fit, best-fit, worst-fit. We provide a precise vocabulary for internal versus external fragmentation. We derive the complexity of each strategy. We prove why fragmentation is unavoidable under naive policies. Finally, we walk through a full worked trace. It shows how the same sequence of requests produces different memory layouts under different algorithms.
- Contiguous allocation gives each process one unbroken block of physical memory, addressed via a base and limit register.
- Fixed partitioning divides memory into preset-size regions; it is simple but wastes space through internal fragmentation.
- Variable partitioning creates exact-size holes on demand but scatters small, unusable gaps — external fragmentation.
- First-fit, best-fit, and worst-fit are the three classic algorithms for choosing which free hole to use.
- No placement policy eliminates fragmentation; each trades allocation speed against space efficiency differently.
- These ideas set up Segmentation and Paging: Basic Concept, which relax the "contiguous" requirement entirely.
Contiguous allocation trades simple, fast address translation for the certainty that memory will eventually fragment, and the placement algorithm only shifts where that waste appears.
The idea
Contiguous memory allocation is a memory-management scheme in which each process is assigned a single, continuous range of physical memory addresses, defined by a base (starting address) and a limit (size, or ending address). When the CPU generates a logical address, hardware adds the base register's value to translate it, and checks that the result stays below base + limit — otherwise it raises a protection fault. This is the direct hardware mechanism underlying Address Spaces and Relocation: relocation becomes a single addition, and protection becomes a single comparison. Two allocation disciplines exist. In fixed partitioning, physical memory is divided in advance into a fixed number of partitions, each of a predetermined size (equal or unequal), and each partition holds at most one process. In variable partitioning (also called dynamic partitioning), the OS carves out a hole of exactly the size a process requests from whatever free memory is available, so no partition boundaries are predetermined. Fixed partitioning is simpler and enables a very fast allocation decision (an array lookup), but leads to internal fragmentation — wasted space inside a partition when the process is smaller than the partition holding it. Variable partitioning fits processes exactly, but as processes come and go, memory becomes riddled with external fragmentation — small free holes between allocated blocks, none individually large enough to satisfy new requests, even though their sum might be. The placement problem in variable partitioning is: given a list of free holes and a new request of size n, which hole should the OS pick? Three classic policies answer this differently — first-fit (the first hole big enough), best-fit (the smallest hole big enough), and worst-fit (the largest hole available) — and each produces a different fragmentation pattern over time, which is the central engineering question this article resolves.
How it works
The OS maintains a data structure describing free memory — typically a linked list or array of `(base, size)` pairs called the free list, kept sorted by address so that adjacent free blocks can be detected and merged. Allocation is the search-and-split operation below; deallocation is the reverse, inserting a freed block back and coalescing it with any physically adjacent free neighbors. ``` // Free list: sorted list of holes, each hole = (base, size) // Allocated list: list of (base, size, owner) for bookkeeping function ALLOCATE(request_size, freelist, strategy): best = null for hole in freelist: // linear scan, O(k) holes if hole.size >= request_size: if strategy == FIRST_FIT: best = hole break // stop at first fit else if strategy == BEST_FIT: if best is null or hole.size < best.size: best = hole // keep smallest sufficient hole else if strategy == WORST_FIT: if best is null or hole.size > best.size: best = hole // keep largest hole if best is null: return ALLOCATION_FAILED // no hole big enough allocated_base = best.base if best.size == request_size: remove best from freelist // exact fit, no leftover else: best.base = best.base + request_size // shrink the hole best.size = best.size - request_size record (allocated_base, request_size) as allocated return allocated_base
function FREE(base, size, freelist): insert (base, size) into freelist, keeping sorted order by base // Coalesce with adjacent free neighbors, O(1) given sorted neighbors if left neighbor of (base,size) is free and left.base + left.size == base: merge left and (base,size) into one hole if right neighbor of (base,size) is free and base + size == right.base: merge (base,size) and right into one hole ``` For fixed partitioning, allocation is simpler: the OS keeps a table of partitions with a busy/free bit, and on request scans (or indexes) for the first free partition of adequate size: ``` function ALLOCATE_FIXED(request_size, partitions): for p in partitions: // usually O(1)-O(k), k = # partitions if p.free and p.size >= request_size: p.free = false return p.base return ALLOCATION_FAILED ``` The figure below shows the state of physical memory under variable partitioning after several allocations and frees, contrasting how first-fit, best-fit, and worst-fit would each place the same next request.
Notice the qualitative difference: first-fit and best-fit both happen to find an exact-size hole here and leave no new fragment, while worst-fit deliberately breaks the largest hole, guaranteeing the leftover remainder (45) is still fairly usable — that is worst-fit's philosophy, even though in general it tends to eliminate large holes quickly, leaving only mid-size ones for future large requests.
Complexity & correctness
Time complexity. All three variable-partition strategies require scanning the free list in the worst case:
- First-fit: O(k) where k is the number of free holes, but averages better in practice because it stops at the first match — empirically close to O(k/2) for uniformly distributed hole sizes.
- Best-fit: O(k) always, since it must examine every hole to find the global minimum sufficient size (unless the free list is kept in a size-sorted balanced tree, reducing search to O(log k) at the cost of O(log k) updates on free/coalesce).
- Worst-fit: O(k) always, for the same reason as best-fit (finding a global maximum), though maintaining a max-heap of holes gives O(log k) if updates are handled carefully.
- Fixed partitioning: O(1) to O(k) depending on whether partitions are indexed by size class or scanned linearly; with k fixed partitions and a bitmap of free/busy, allocation is a bitmap scan, O(k) worst case, O(1) with a free list per size class.
Deallocation is O(1) amortized in all schemes if the free list is a doubly linked list sorted by address (coalescing checks only the immediate left/right neighbor), or O(log k) / O(k) if maintained sorted by size and re-insertion requires repositioning.
Space overhead is O(k) for the free-list metadata itself (a handful of words per hole), independent of process count — cheap compared to the memory being managed.
Correctness argument. The allocator must satisfy two invariants at all times: 1. Disjointness invariant: no two allocated blocks, and no allocated block and free hole, overlap in address range. 2. Completeness invariant: every byte of managed memory is accounted for as either exactly one allocated block or exactly one free hole. `ALLOCATE` preserves both: it only ever shrinks an existing hole from one end (updating `base` and `size` together) or removes it entirely on an exact match, so the union of all holes and allocations never changes and no overlap is introduced — this follows by induction on the number of allocation calls, since the base case (empty allocation history) trivially satisfies both invariants, and each call is a pure split operation on one hole. `FREE` preserves the invariants symmetrically: it reinserts the freed range as a hole (restoring completeness) and then merges with adjacent free neighbors only when their address ranges are exactly touching (`left.base + left.size == base`), which by the disjointness invariant on holes guarantees the merged region is still a single, non-overlapping contiguous hole.
Why fragmentation is provably unavoidable in the long run. Consider variable partitioning under any placement policy. As processes of varying sizes arrive and depart in essentially arbitrary order, the sequence of splits (which shrink holes without ever recombining differently-shaped chunks except via exact address-adjacency) implies that holes can only be merged if they are adjacent survivors — but nothing prevents an allocated block from permanently separating two holes that would otherwise merge into something large enough for a future request. This is a direct consequence of the disjointness invariant: a live process occupying the middle of memory is an immovable wall (without relocation, which contiguous allocation without compaction does not perform), so external fragmentation accumulates as a structural certainty, not merely a bad-luck outcome. Analytically, under randomized request/release patterns, the well-known 50% rule (derived by Knuth) states that if there are n allocated blocks, the expected number of holes is about n/2, so roughly one-third of memory can become unusable fragments even with optimal-looking policies — this is a statistical regularity, not an artifact of a specific algorithm.
Honest tradeoffs.
- First-fit is fast and tends to leave larger holes toward the high-address end of memory, but accumulates small unusable slivers near the front where scanning always begins.
- Best-fit minimizes the size of leftover fragments per allocation, which sounds optimal, but this is precisely why it is bad in aggregate: it manufactures many tiny, likely-useless holes, since "closest fit" leftovers are by construction small.
- Worst-fit avoids tiny leftovers per-call but destroys the few large holes fastest, which can starve future large requests even though small requests are served comfortably.
- Fixed partitioning avoids external fragmentation entirely (partition boundaries never move) but guarantees internal fragmentation whenever process size doesn't match partition size — and if partition sizes are unequal but static, some size classes may be perpetually oversubscribed while others sit idle.
None of these can be "fixed" purely by algorithm choice; the underlying limitation is contiguity itself, which is exactly the motivation for relaxing it in Segmentation and Paging: Basic Concept.
Worked example
Suppose physical memory (excluding a fixed 30-unit OS region) spans addresses 30–300, giving 270 units of user memory, initially one giant hole. Requests arrive and depart in this order:
| Step | Event | Size | Notes |
|---|---|---|---|
| 1 | Allocate A | 60 | |
| 2 | Allocate B | 100 | |
| 3 | Allocate C | 40 | |
| 4 | Free B | — | opens a 100-unit hole |
| 5 | Allocate D | 90 | must fit in freed hole or elsewhere |
| 6 | Allocate E | 30 |
We trace first-fit and best-fit side by side. Free list starts as `[(30, 270)]`.
| Step | First-fit free list after step | Best-fit free list after step |
|---|---|---|
| 1. Alloc A(60) | A@30–90. Free: `[(90,210)]` | A@30–90. Free: `[(90,210)]` |
| 2. Alloc B(100) | B@90–190. Free: `[(190,110)]` | B@90–190. Free: `[(190,110)]` |
| 3. Alloc C(40) | C@190–230. Free: `[(230,70)]` | C@190–230. Free: `[(230,70)]` |
| 4. Free B(90,100) | Free: `[(90,100),(230,70)]` (not adjacent — C sits between; no coalescing) | same: `[(90,100),(230,70)]` |
| 5. Alloc D(90) | Scans left→right: hole(90,100) fits first → D@90–180. Free: `[(180,10),(230,70)]` | Compares both: hole(100) fits, hole(70) doesn't. Only one candidate → D@90–180. Free: `[(180,10),(230,70)]` |
| 6. Alloc E(30) | Scans: hole(180,10) too small, hole(230,70) fits → E@230–260. Free: `[(180,10),(260,40)]` | Only hole(70) qualifies (10 too small) → same: E@230–260. Free: `[(180,10),(260,40)]` |
In this particular trace, first-fit and best-fit coincide because at each step there was only one hole large enough to matter — the divergence between algorithms only shows up when multiple holes simultaneously satisfy a request. Let's continue with a divergent step:
| 7. Alloc F(10) | First-fit scans: hole(180,10) is an exact match, first one checked → F@180–190. Free: `[(260,40)]` | Best-fit scans both: hole(180,10) is exact match (size 10 = request 10), hole(260,40) is not minimal → same F@180–190. Free: `[(260,40)]` |
|---|
Here, too, they coincide — the (180,10) hole is both first and smallest. Now push one more divergent case:
| 8. Alloc G(15) | First-fit: only hole is (260,40), which fits → G@260–275. Free: `[(275,25)]` | Best-fit: same, only one candidate → G@260–275. Free: `[(275,25)]` |
The two algorithms only visibly diverge in placement when the free list simultaneously contains two-or-more holes that both satisfy a request — a scenario easy to construct (e.g., two 200-unit free holes and a 205-unit hole with a request for 190 would make best-fit pick the 200-unit ones while first-fit picks whichever appears first in scan order), and this is a faithful reflection of reality: the algorithms behave identically on many inputs and differ only on specific adversarial-looking configurations, which is exactly why choosing between them in a real allocator requires workload-specific tuning rather than a universal "best" answer. Final state after step 8: memory holds A, C, D, F, G allocated with a single trailing hole of 25 units — external fragmentation is modest here because C sits as an isolated block between B's old space and the tail, but note that no future request larger than 25 can be satisfied without either relocating C or waiting for further frees, even though total free space could exceed the request if it were contiguous — the textbook signature of external fragmentation.
Common pitfalls & traps
| Pitfall | Why it happens | Fix |
|---|---|---|
| Forgetting to coalesce adjacent free blocks on `FREE` | Treating free list as append-only | Always check both neighbors by address adjacency before inserting |
| Off-by-one in limit check (`addr <= base+limit` vs `<`) | Confusing size with last valid address | Define limit as size, so valid range is `[base, base+size)`, checked with strict `<` |
| Assuming best-fit minimizes total fragmentation | Confusing "minimal single leftover" with "best global outcome" | Recognize best-fit generates many tiny slivers over time — proven pathological in practice |
| Using fixed partitions sized for the "average" process | Ignoring the distribution's variance | Track per-size-class demand or switch to variable partitioning / paging |
| Ignoring failed allocation on a false "free memory exists" check | Confusing total free bytes with largest contiguous free block | Always compare request size against the largest single hole, not the sum |
| Neglecting that internal fragmentation still happens with equal-size partitions | Assuming uniform partitions are "immune" to waste | Internal fragmentation only vanishes if partition size == process size, essentially never guaranteed |
Pioneers & history
Contiguous allocation was the default memory model in the earliest multiprogrammed operating systems, when memory was scarce, expensive, and treated as one physical resource to be sliced directly — long before virtual memory decoupled logical from physical addresses.
Origins · 1 minKenneth C. Knuth (Donald E. Knuth) and how this was discovered
Where this matters
Contiguous allocation concepts persist wherever a resource must be handed out as one unbroken chunk. Embedded and real-time operating systems (many RTOS memory pools, small IoT kernels) still use fixed or variable partition schemes directly, because their memory footprints are small and predictable enough that fragmentation is manageable and the simplicity/speed of base-limit translation is worth it. General-purpose kernel memory allocators — the buddy system and slab allocators in Linux, for instance — descend conceptually from these placement ideas, managing physically contiguous pages for DMA-capable devices and kernel objects that cannot tolerate scattered pages. GPU memory managers and some database buffer pools also allocate contiguous regions for large sequential buffers where address-translation overhead must stay minimal. Understanding first-fit, best-fit, and worst-fit — and precisely why each produces internal versus external fragmentation — is the essential stepping stone to why virtual memory systems introduced Segmentation (contiguous logical segments mapped flexibly onto physical memory) and ultimately Paging: Basic Concept (fixed-size frames that eliminate external fragmentation by design, at the cost of new problems like internal fragmentation within a page and translation overhead).
Ask the tutor: How does the buddy system allocator resolve some of these fragmentation trade-offs while still allocating physically contiguous memory?
Why doesn't contiguous allocation just always use best-fit if it minimizes waste per request?
Best-fit minimizes waste locally, for a single allocation, but this local optimality doesn't translate into global efficiency. By always choosing the smallest sufficient hole, best-fit systematically produces leftover fragments that are tiny — often too small to satisfy any realistic future request. Over many allocation/free cycles, memory becomes littered with these unusable slivers, a phenomenon confirmed both analytically (Knuth's fragmentation studies) and empirically in allocator benchmarks. First-fit, despite being "greedier" and less careful, often performs comparably or better in practice because it doesn't deliberately manufacture tiny holes, and it's also faster since it can stop scanning early. This is a classic lesson in algorithm design: local optimality and global optimality frequently diverge, and picking a policy requires measuring actual workload behavior, not just intuition about "wasting less" per step.
Does fixed partitioning ever make sense in modern systems?
Yes, particularly in embedded and real-time systems where predictability outweighs raw memory efficiency. Fixed partitions guarantee constant-time allocation (an array or bitmap lookup) with zero external fragmentation, since partition boundaries never shift — a property real-time schedulers value because allocation latency becomes bounded and analyzable. The cost is internal fragmentation: memory reserved but unused within each partition. For systems with well-known, narrow process size distributions (a fixed set of sensor tasks, for example), this waste is small and predictable, making fixed partitioning an entirely reasonable engineering choice even today, despite general-purpose operating systems having moved to paging decades ago.
How does compaction relate to contiguous allocation, and why isn't it used more often?
Compaction is the technique of shifting allocated blocks in physical memory to merge scattered free holes into one large contiguous region, directly attacking external fragmentation. It requires that processes be relocatable at runtime — meaning all their memory references go through a base register that the OS can update after moving the process, tying compaction closely to the relocation mechanisms in Address Spaces and Relocation. The catch is cost: compaction requires pausing affected processes and physically copying potentially large amounts of memory, an O(n) operation in total bytes moved, which can stall the system noticeably. Because of this overhead, most modern systems instead sidestep contiguity requirements altogether via paging, rather than paying compaction's runtime tax.
What exactly is the "50% rule" and why does it matter?
Knuth's 50% rule is an empirical/analytical result stating that under random allocation and deallocation patterns with first-fit or best-fit, the number of free holes tends toward roughly half the number of allocated blocks, and roughly one-third of all memory becomes fragmented into unusable pieces in equilibrium. It matters because it shows fragmentation is not a rare edge case or implementation bug — it's a statistical consequence of the allocation model itself, given realistic, non-adversarial workloads. This result is part of why operating system designers eventually concluded that no clever placement heuristic could fully solve contiguous allocation's structural problem, motivating the shift to paging, where fixed-size frames make external fragmentation impossible by construction.
How is address translation actually done in a contiguous-allocation system?
The CPU carries a base register and a limit register (sometimes combined into a single relocation/bounds pair) that are loaded by the OS during a context switch, reflecting where the currently running process's memory block starts and how large it is. Every logical address the process generates is checked against the limit (raising a trap if it's out of bounds) and then added to the base register to produce the physical address, entirely in hardware, adding negligible latency. This is the simplest possible instance of the general relocation mechanism described in Address Spaces and Relocation — a single addition and comparison, versus the multi-level table walks required later in paging.
Why do first-fit and best-fit sometimes produce identical results, as in the worked example?
Both algorithms only diverge in behavior when the free list simultaneously contains multiple holes that are each individually large enough to satisfy the current request. If only one hole in the entire free list qualifies, both algorithms necessarily choose it — first-fit because it's the only one encountered during the scan, best-fit because it's trivially both the smallest and largest sufficient candidate. Divergence requires a free-list configuration with at least two "big enough" holes of different sizes appearing in different scan positions, which is common under sustained, varied churn but can be entirely absent in short traces, as the worked example shows.
Could contiguous allocation causeprocesses to fail even with abundant total free memory?
Yes — this is precisely external fragmentation's danger. If total free memory is, say, 500 units spread across five separate 100-unit holes, but a process requests 150 contiguous units, the allocation fails outright even though 500 > 150. Contiguous allocation fundamentally requires one unbroken region; scattered free space, however large in aggregate, cannot satisfy a request larger than its biggest single hole. This is exactly the motivating failure mode that paging solves, since paging never requires physical contiguity — any set of free frames, however scattered, can back a process's address space.