In this lecture we investigate memory allocation! We begin with studying contiguous memory allocation techniques like fixed and dynamic partitioning. We then use these techniques to investigate the problem of fragmentation and setup the foundation for virtual memory!
Lecture Date
📅 March 16, 2026
Standard
Memory
Topics Covered
Contiguous AllocationFixed PartitioningDynamic PartitioningFragmentationLogical vs Physical Addresses
In LN13, we surveyed the full landscape of memory hardware — from SRAM registers at picosecond speeds to tape drives measured in seconds. We established the mental model of memory as a series of finite strips ("rolls of toilet paper"), each a different length, with processes coloring parts of every roll simultaneously.
Today we face the critical question: how do we actually allocate processes onto those strips? We have fast memory that is tiny (registers, cache) and slow memory that is enormous (HDDs, tape). Running processes need to live in the fast memory — RAM — but that space is limited. We need to be smart about how we place processes into it, because every decision we make about placement has consequences for performance, waste, and system stability.
Today's Agenda
The Goal — Placing processes into finite RAM
Contiguous Allocation — Requiring each process to occupy a single continuous block
Fixed Partitioning — Pre-divided memory regions ("bar seating")
Dynamic Partitioning — Variable-size regions on demand ("booth seating")
Allocation Strategies — First-Fit, Best-Fit, Worst-Fit, and how they compare
The Problem with Contiguous Allocation — Why fragmentation forces a rethink
Logical vs. Physical Addresses — Separating the programmer's view from hardware reality
The Memory Management Unit — Hardware that bridges the two address worlds
The Goal
Our goal is to take the memory model from last time and allocate all data — whether being used by active processes or set aside for storage — across our strips. We need to be cognizant of what data needs to be close to the CPU and just how much to move there, since the space gets smaller and smaller the faster we need it.
Since only running processes need high-speed access, we will focus on getting processes placed into main memory (RAM). The question becomes: given a finite strip of RAM, how do we fit multiple processes onto it?
The simplest answer is also the most intuitive: give each process its own contiguous block.
Contiguous Allocation
This is how you would naturally place things on a shelf: each item gets its own stretch of space, packed next to the previous one. Simple, predictable, fast to access. But as we will see, it comes with significant tradeoffs. There are two major techniques for contiguous allocation.
Fixed Partitioning — "Bar Seating"
The first technique is fixed partitioning. Think of it like bar seating at a restaurant — the stools are evenly spaced and bolted to the floor. You sit in however many stools your group needs, but you cannot move them.
In the simplified model for this lecture, memory is divided into equal-sized allocation units ahead of time. If a process needs 18 KB and each unit is 16 KB, it receives 2 units (32 KB total) even though it only uses 18 KB.
Before you touch the slider, predict which direction reduces waste inside an allocated block: larger units or smaller ones.
Fixed Partitioning — Internal Fragmentation Explorer
Drag the slider to change the partition size. Watch how internal fragmentation (wasted space within partitions) changes.
Partition size: 16 KB8 partitions
Memory (128 KB total — 8 partitions of 16 KB)
P1
P1
P2
P3
P3
P4
P5
0 KB128 KB
P1
18 KB
+14 KB waste
P2
7 KB
+9 KB waste
P3
31 KB
+1 KB waste
P4
12 KB
+4 KB waste
P5
5 KB
+11 KB waste
Allocated
112 KB
Actually Used
73 KB
Internal Frag.
39 KB
Waste %
34.8%
Therefore, fixed-size allocation trades tracking simplicity for internal waste: smaller units reduce waste, but require more bookkeeping.
Notice what happens as you adjust the partition size: if a process is not perfectly divisible by the partition size, there is overallocation. Those wasted bytes within the allocated partitions are called internal fragmentation.
By making the partition size smaller, we can match process sizes more closely, reducing internal fragmentation. But there is a catch: as the partition size decreases, we need more partitions, and we need a data structure to track whether each partition is in use. A partition size of 1 byte would theoretically eliminate all waste — but the tracking data structure would be as large as the memory itself! We would need as much memory to manage memory as we have to use.
So increasing partition size lets us sacrifice less overhead to management, but the larger we go, the worse the internal fragmentation gets. This is a fundamental tradeoff with no perfect answer in fixed partitioning.
💡 Connection to LN13: This tradeoff echoes the memory hierarchy itself — more capacity means less speed, more speed means less capacity. Fixed partitioning makes you choose where on that spectrum to sit, and there is no universally right answer.
But wait — if we can measure the internal fragmentation, it is because we already know the size of each process. Why do we not just allocate partitions that match the process size, rather than choosing a fixed size ahead of time?
Dynamic Partitioning — "Booth Seating"
This is the logic behind dynamic partitioning. Think of it like booth seating — people slide into the booth and take up only the space they need. No wasted room.
The idea is to separate memory into regions sized to each arriving process. When a process arrives, we carve out exactly as much memory as it needs. No overallocation, no internal fragmentation. Problem solved!
...or is it?
⚠️ Watch For This: The specific mismatch to track is that total free memory can be large enough while the largest contiguous gap is still too small.
Dynamic Partitioning — External Fragmentation
Step through process arrivals and departures. Watch gaps form between processes, then try compaction.
P1
80
0 KB100 KB
Step 1/10: P1 (20 KB) arrives — allocated at front
Used
20 KB
Free (total)
80 KB
Largest Contig.
80 KB
Gaps
1
Therefore, dynamic partitioning fixes the "waste inside the block" problem by making it easier to create the "waste between blocks" problem.
Step through the events above and watch what happens when processes terminate. Gaps of varying sizes form between the remaining processes. Eventually, a large process arrives and gets rejected — even though the total free memory across all gaps is more than enough! The problem is that no single contiguous gap is large enough.
This problem also appears in fixed partitioning (unallocated partitions scattered between used ones), so it is not unique to dynamic partitioning. But dynamic partitioning makes it especially visible because the gap sizes are irregular.
Taking inspiration from restaurants — what if we asked everyone to slide down to fill the gaps? We could alleviate fragmentation by simply reallocating everything to pack processes together.
Compaction works — but it becomes increasingly expensive as memory gets larger. Every byte of every process must be copied. As systems get more RAM and processes get larger, compaction takes more and more time. We need a better approach: can we prevent fragmentation through smarter allocation?
Allocation Strategies
We know the fragmentation problem is caused by small processes being allocated next to or between larger ones, leaving awkward gaps when the small ones terminate. What if we were smarter about which gap we choose?
Let us categorize what we have been doing: First-Fit — allocate at the first gap that is large enough, scanning from the beginning of memory. Simple and fast. But there are alternatives:
Strategy
Description
First-Fit (FF)
Allocate at the first gap found in a linear scan from the start
Best-Fit (BF)
Scan all gaps, pick the smallest one that fits — minimize leftover space
Worst-Fit (WF)
Scan all gaps, pick the largest one — maximize leftover space
Next-Fit (NF)
Like First-Fit, but start scanning from where you left off last time
Try each strategy on the same sequence of events:
🤔 Think About This: As you step through the demo, do not just ask whether the allocation succeeds. Ask which gap each strategy chooses, and why.
Select a strategy, then step through the same sequence of arrivals and departures to compare behavior.
A
100
0 KB120 KB
Scan behavior
0-120 KB (120 KB)
First usable gap in a linear scan from the front.
Step 1/13: A (20 KB) → allocated at 0-20 KB
Used
20 KB
Free
100 KB
Gaps
1
Failures
0
These algorithms have been studied extensively. Based on empirical and theoretical analysis, we can generally rank them by overall performance. It should be strongly noted however that with a better understanding our your workload, these algortihms may reorder:
Pop Quiz — Rank the Allocation Algorithms
Based on extensive empirical study, these four algorithms can be generally ranked by overall performance (speed + fragmentation management). Before revealing the answer, think about which one you would expect to win — and which would come last.
First-FitBest-FitWorst-FitNext-Fit
Which is fastest overall? Which is slowest? Can you guess?
Amazing results, right? The irony of the naming — "Best-Fit" is actually worse than "Worst-Fit"! But the real question is why. In many areas of programming, trying to proactively address issues earlier almost always leads to improvement. So how did the naive First-Fit take first place, and the technique that tried its hardest to prevent fragmentation end up being the slowest?
The answer comes from understanding process behavior.
Processes with smaller memory needs are generally faster — they need less space for computation and tend to terminate quickly. Larger processes with greater needs generally run longer. This asymmetry is what causes external fragmentation: small processes allocated next to large ones leave tiny gaps when they exit, while the large processes remain.
So why does not Best-Fit help? Because it actually creates more small gaps. By always picking the smallest fitting gap, every allocation leaves a tiny sliver of leftover space. After many cycles, memory is peppered with unusable 1-2 KB gaps. Compaction has to handle far more fragments, even though each is small.
Worst-Fit does better because placing small processes in the largest gap leaves large remaining gaps — which are more likely to be useful for future large processes.
Next-Fit is not great because it distributes gaps evenly across memory without any management strategy, leading to fragmentation everywhere.
📚 Historical Note: Donald Knuth analyzed these allocation strategies in The Art of Computer Programming (1973) and established the "Fifty-Percent Rule" — under First-Fit, roughly half of all allocated blocks are adjacent to a free block. This empirical finding has held up across decades of real workloads.
And First-Fit? It works best for the same reason Multi-Level Feedback Queues were revolutionary for scheduling: it naturally sorts by behavior! Small processes appear quickly and leave quickly. They form small gaps at the front of memory (since FF always starts scanning from the beginning). This pushes larger, slower processes toward the back. Over time, memory self-organizes into a hot zone at the front (many tiny fast-turnover processes) and a cold zone at the back (stable large processes). The small processes naturally fit in the small front gaps, leaving large gaps at the back for large processes.
Less management is better in this space. Some jobs actually require less supervision to succeed.
Before using the visual below, watch two things at once: where new allocations land and what shape of free space is left behind
Why Does First-Fit Win? — Allocation Behavior
Each tab shows what memory looks like after many allocation cycles with that strategy. Notice the gap patterns.
s2
s4
BIG-1
BIG-2
33
Natural behavioral sorting
Small processes churn at the front, leaving the back for large, stable processes. The memory self-organizes into a hot zone (fast turnover) and a cold zone (slow turnover) — just like MLFQ naturally sorts by burst length!
Gaps: 4Avg gap size: 10.0 KB
Hot Zone (front)
Small fast processes churn here
Cold Zone (back)
Large stable processes settle here
Therefore, allocator quality is really about the long-term shape of free space, not just whether one individual allocation succeeds.
The Problem with Contiguous Allocation
Both internal and external fragmentation are direct consequences of requiring everything to be contiguous. So what if we did what the scheduler does and split a process in half? Allocate the first half in one gap and the rest somewhere else?
Wait. Before we do such a thing, consider the ramifications. Machine code relies on ISA instructions being one after another in the process address space. All the jumps, all the program counter increments — they assume sequential layout. What would your programs look like if we just split them arbitrarily?
Split by file? Would not work for scripts, only compiled object-oriented languages.
Split by function? The management overhead would be enormous, and not all programs are structured that way.
Split line by line? We would need entirely new ways of visualizing computation. Every line being independently placed means reading top-to-bottom is worthless. Every line becomes its own thread? Programmers would revolt.
The programmer's response is a resounding NO. Programming is already hard enough without having to write code that anticipates being sliced to pieces. But we absolutely need non-contiguous allocation to fix fragmentation!
So we need both: non-contiguous allocation for the memory hardware, and the illusion of contiguous addresses for the programmer. Can we have our cake and eat it too?
To find out, we need to reconcile how programmers and hardware each use addresses.
Logical vs. Physical Addresses
Let us start from the programmer's perspective and work our way down, then come up from the hardware side, and see where the disconnect lies.
The Programmer's View
When we write programs, we use variable names, function names, and line numbers. The entire toolchain you are using, including the compiler, linker, and loader, translates these into machine code with addresses — numerical labels that identify where values live and where instructions jump to.
Before stepping through the mapping, watch what stays stable for the program: names and relative relationships inside the code.
Logical Addresses — Code to Machine Code
Hover over lines to see the mapping. Toggle between two completely different programs and notice: they use the exact same addresses.
A simple function that sums two values
Rust Source Code
fn main() {
let x = 10;
let y = 20;
let sum = x + y;
println!("{}", sum);
}
Machine Code (Assembly)
0x0000PUSH RBP
0x0008MOV [RBP-8], 10
0x0010MOV [RBP-16], 20
0x0018ADD RAX, [RBP-8]
0x0020ADD RAX, [RBP-16]
0x0028CALL _print
0x0030POP RBP
0x0038RET
Notice: Both programs start at 0x0000 and use the exact same address range. Variable x lives at RBP-8 in both. If we placed both programs in physical memory at these addresses simultaneously, they would collide. Every program thinks it owns all of memory starting from 0.
Therefore, from the program's point of view, addresses behave like local labels inside one address space.
Two critical observations:
Every program starts at address 0. The compiler assigns addresses from 0 upward for every program independently.
Different programs reuse the same addresses. Two completely different programs both have variables at RBP-8, both start execution at 0x0000.
From the programmer's perspective, addresses are just names — convenient labels for the things in our program. We use them like names at a restaurant: "Table for x, party of y."
The Hardware's View
Now consider the machine code executing on the CPU. Recall that 64-bit ISA instructions literally map onto logic gates. An address in machine code physically selects a specific location in memory hardware. Only one piece of data can occupy a given physical address at any time. Addresses are not names — they are slots.
If we mapped every program's logical addresses directly to physical memory, every program would try to occupy the same physical slots — starting at 0x0000. Data corruption. Crashes. Chaos.
The Fix: Base Offset Translation
Both types of addresses are just numbers. We have a guarantee from programmers that all logical addresses are entirely contained within their process. We have a hardware guarantee that every physical address slot is unique and treated equally.
If mapping logical addresses directly to physical addresses results in collisions, why do we not simply offset each process's logical addresses until they land on an unused region of physical memory? The process slides into the available space while keeping all its internal address relationships intact.
📌 Key Point: Track one number carefully: every process begins at the same logical start, but ends up with a different physical start after translation.
Before stepping through this demo, track one invariant: the program's logical address stays the same even though the physical destination changes.
Address Translation — Logical to Physical
All of these processes start at logical address 0x0000. Watch what happens when we map them directly to physical memory, then apply base offsets to slide each one into its own physical region.
PhaseCollision
Physical Memory (384 KB)
P1 (sum)
P2 (product)
P1' (sum copy)
P3 (parser)
P4 (shell)
COLLISION!
0x00000x00400x00800x00C00x01000x01400x0180
Process
Logical Start
Base Offset
Physical Start
Physical End
Translation
P1 (sum)
0x0000
—
0x0000
0x0027
0x0000 + — = 0x0000
P2 (product)
0x0000
—
0x0000
0x001D
0x0000 + — = 0x0000
P1' (sum copy)
0x0000
—
0x0000
0x0027
0x0000 + — = 0x0000
P3 (parser)
0x0000
—
0x0000
0x0017
0x0000 + — = 0x0000
P4 (shell)
0x0000
—
0x0000
0x0023
0x0000 + — = 0x0000
Problem: Every process begins at logical address 0x0000, so direct mapping makes them collide immediately. Even P1' (a duplicate of P1) still needs its own physical slot range.
Therefore, translation lets the programmer keep one coherent address space while the OS chooses a different physical placement.
The formula is beautifully simple:
physical address = logical address + base
In real systems, this simple model is usually paired with a limit/bounds check so the process cannot wander outside its assigned region.
This works because the translation is systematic: one logical region maps to a shifted physical region. The mechanism is hardware-assisted, while the operating system is responsible for setting up the mapping correctly.
Preview: The Memory Management Unit
This hardware offset is computed by the MMU (Memory Management Unit) — a dedicated piece of hardware that sits between the CPU and memory. Every address the CPU produces is a logical address. The MMU intercepts it, adds the base offset for the current process, and sends the translated physical address to the memory bus.
💡 Connection to LN8: The MMU is hardware we already placed on our CPU diagram — it was sitting between the CPU and memory the whole time. Now we finally see its full job description: not just protecting memory boundaries, but actively translating every single address the CPU produces.
This is the same MMU we mentioned back in LN9 when discussing CPU security — it was already there, enforcing memory boundaries. Now we see its other critical role: address translation.
Next time, we will explore the MMU in depth and discover that the simple base-offset scheme we just derived is actually the foundation of something much more powerful: Virtual Memory. Instead of one base offset per process, what if we could translate every page of a process independently? That is where non-contiguous allocation finally becomes real — without the programmer noticing a thing.
Summary
Contiguous allocation gives each process a single unbroken block of addresses
Fixed partitioning divides memory into equal regions — simple but causes internal fragmentation (waste inside partitions)
Dynamic partitioning sizes partitions to each process — eliminates internal fragmentation but creates external fragmentation (gaps between processes)
Compaction fixes fragmentation by relocating processes, but is expensive
Four allocation strategies: FF (scan from start), BF (smallest gap), WF (largest gap), NF (scan from last position)
In many workloads, First-Fit performs well, but allocator rankings are empirical and workload-dependent
Contiguous allocation fundamentally causes fragmentation; non-contiguous allocation is needed but programmers refuse to restructure their code
Logical addresses (programmer names, start at 0) vs. physical addresses (hardware slots, must be unique)
A simple base offset translates logical to physical; real systems pair that idea with bounds/protection checks
Next lecture: Virtual Memory — independent translation of every page