CD Notes
Comprehensive guide to memory management strategies, caching, and optimization
Introduction to Memory Management
Memory management is one of the most critical aspects of a runtime environment. Think of it as the "real estate management" for your program — deciding where data lives, how long it stays, and how to reclaim space when it is no longer needed. A compiler must generate code that correctly interacts with the memory management system, whether that means explicit allocation calls in C or implicit garbage collection triggers in Java.
Understanding memory management at multiple levels — hardware, operating system, and application — gives you insight into why programs behave the way they do, why some are fast and others slow, and why certain bugs (memory leaks, segmentation faults, cache misses) occur.
The Memory Hierarchy
Modern computers organize memory in a hierarchy, trading off speed for capacity at each level:
| Registers | ~1 ns | ~64 bytes | CPU |
|---|---|---|---|
| L1 Cache | ~2 ns | 32-64 KB | CPU |
| L2 Cache | ~7 ns | 256 KB-1 MB | CPU |
| L3 Cache | ~15 ns | 4-64 MB | Shared |
| Main Memory | ~100 ns | 4-128 GB | RAM |
| SSD/Disk | ~100 μs | 256 GB-TB+ | Disk |
Notice that main memory (RAM) is roughly 100 times slower than L1 cache. This is why compilers spend enormous effort on register allocation and data locality optimizations — keeping data in the fastest level of the hierarchy makes programs run dramatically faster.
Virtual Memory: The Illusion of Unlimited RAM
The operating system provides each program with a virtual address space that appears much larger than physical RAM. This abstraction, called virtual memory, uses the disk as an overflow area:
| Process A sees | 0x00000000 to 0xFFFFFFFF (4 GB virtual) |
| Process B sees | 0x00000000 to 0xFFFFFFFF (4 GB virtual) |
| The OS uses page tables to map virtual | physical addresses: |
| Virtual page 0x1000 | Physical frame 0x5A000 |
| Virtual page 0x2000 | Physical frame 0x3B000 |
| Virtual page 0x3000 | [on disk, not in RAM] |
When a program accesses a page that is not in physical memory, a page fault occurs. The OS loads the page from disk into RAM (possibly evicting another page first). This is transparent to the program but causes a significant performance hit — disk access is thousands of times slower than RAM access.
Application-Level Memory Management
At the application level, the compiler and runtime system manage three main memory regions:
Static memory — For global variables and constants. Allocated when the program loads, freed when it terminates. The compiler places these in the .data and .bss sections of the executable.
Stack memory — For local variables and function call management. Managed automatically through the stack pointer. Extremely fast but limited in lifetime (tied to function scope) and size.
Heap memory — For dynamically allocated data. Managed either manually (malloc/free in C) or automatically (garbage collection in Java/Python). Flexible but more complex and slower than stack allocation.
Garbage Collection Strategies In Depth
Reference Counting
The simplest GC approach: attach a counter to every object tracking how many pointers reference it.
Implementation overhead: Every pointer assignment requires incrementing and decrementing counters. The compiler generates extra instructions for every reference change.
Cycle problem: If object A points to B and B points to A, both have refcount 1 even when unreachable. Python solves this with a supplementary cycle-detecting collector.
Mark-and-Sweep
This collector pauses program execution, marks all reachable objects starting from roots, then frees everything unmarked:
Generational Garbage Collection
The key insight of generational GC is the weak generational hypothesis: most objects die young. A newly created string in a loop iteration is typically dead by the next iteration. By collecting the "young generation" frequently and the "old generation" rarely, we minimize GC pauses:
Young Generation (nursery)
Size: Small (a few MB)
Collection frequency: Very often (milliseconds)
Promotion: Objects surviving N collections move to old gen
Algorithm: Copying collector (fast for mostly-dead heaps)
Old Generation (tenured)
Size: Large (most of heap)
Collection frequency: Rare (seconds to minutes)
Algorithm: Mark-compact or concurrent mark-sweep
The JVM's G1 garbage collector divides the heap into hundreds of equal-sized regions and collects the regions with the most garbage first (hence "Garbage First"), achieving predictable pause times.
Concurrent and Incremental Collection
Modern garbage collectors run concurrently with the application to avoid long pauses. The Go garbage collector, for example, marks objects while the program continues running, using write barriers to track reference changes that happen during marking. This keeps pause times under 1 millisecond even for heaps of many gigabytes.
Memory Optimization Techniques
Cache Locality
Accessing memory in sequential order (spatial locality) or repeatedly accessing the same location (temporal locality) dramatically improves performance because cache lines are loaded in chunks:
Object Pooling
Pre-allocate a pool of objects and reuse them instead of repeatedly calling malloc/free:
Memory-Mapped I/O
For large files, map them directly into virtual memory instead of reading into buffers. The OS loads pages on demand as they are accessed:
int fd = open("large_file.dat", O_RDONLY);
void *mapped = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
// Access mapped memory as if it were a regular array
// OS handles loading from disk transparentlyThe Compiler's Role
The compiler makes many memory-related decisions:
- Register allocation: Keeping hot variables in registers instead of memory
- Stack layout: Ordering local variables for optimal alignment and cache behavior
- Escape analysis: Determining whether an object can be stack-allocated instead of heap-allocated (a major optimization in Java JIT compilers)
- Allocation call generation: Inserting appropriate runtime calls for
new,malloc, or GC allocation - Write barrier insertion: For generational GC, inserting barriers when old-generation objects reference new-generation objects
Common Memory Bugs and Their Causes
| Bug | Cause | Language Affected |
|---|---|---|
| Memory leak | Allocated but never freed | C, C++ |
| Use-after-free | Accessing freed memory | C, C++ |
| Buffer overflow | Writing beyond allocation | C, C++ |
| Stack overflow | Excessive recursion | All languages |
| Out of memory | Exhausting heap | All languages |
| False sharing | Cache line contention | Multi-threaded |
Key Takeaways
Memory management spans the entire system from hardware caches to application-level garbage collection. The compiler plays a central role by deciding where variables live (register, stack, or heap), generating allocation and deallocation calls, and optimizing memory access patterns for cache efficiency. Understanding this multi-level system helps you write programs that are both correct (no leaks or corruption) and efficient (good cache behavior, minimal allocation overhead).
Exam Focus
Revise definitions, diagrams, examples, and short-answer points for Memory Management: Strategies and Optimization.
Interview Use
Prepare one clear explanation, one practical example, and one common mistake for this Compiler Design topic.
Search Terms
compiler-design, compiler design, compiler, design, runtime, environment, memory, management
Related Compiler Design Topics