Memory is a vector of pairs. Garbage collection reclaims unreachable storage. Stop-and-copy GC compacts live data by copying it to new space, then flipping.
Memory as vectors
At the lowest level, memory is a flat array of numbered slots. Lisp pairs become two parallel vectors: the-cars and the-cdrs. Index n in both vectors together form one pair. A pointer is just an index into these vectors.
Scheme
; Memory as two parallel vectors: the-cars and the-cdrs.; A cons cell at index i stores car in the-cars[i], cdr in the-cdrs[i].
(define memory-size 8)
(define the-cars (make-vector memory-size 'free))
(define the-cdrs (make-vector memory-size 'free))
(define free 0)
(define (my-cons car-val cdr-val)
(let ((idx free))
(set! free (+ free 1))
(vector-set! the-cars idx car-val)
(vector-set! the-cdrs idx cdr-val)
idx)) ; return the pointer (index)
(define (my-car ptr) (vector-ref the-cars ptr))
(define (my-cdr ptr) (vector-ref the-cdrs ptr))
; Build the list (a b c)
(define p2 (my-cons 'c '()))
(define p1 (my-cons 'b p2))
(define p0 (my-cons 'a p1))
(display "list starts at index: ") (display p0) (newline)
(display "car[0]=") (display (my-car p0))
(display " car[1]=") (display (my-car p1))
(display " car[2]=") (display (my-car p2)) (newline)
(display "cdr[0]=") (display (my-cdr p0))
(display " cdr[1]=") (display (my-cdr p1))
(display " cdr[2]=") (display (my-cdr p2))
Python
# Memory as two parallel arrays
MEMORY_SIZE = 8
the_cars = [None] * MEMORY_SIZE
the_cdrs = [None] * MEMORY_SIZE
free = 0def my_cons(car_val, cdr_val):
global free
idx = free
free += 1
the_cars[idx] = car_val
the_cdrs[idx] = cdr_val
return idx # pointer = indexdef my_car(ptr): return the_cars[ptr]
def my_cdr(ptr): return the_cdrs[ptr]
# Build the list (a b c)
p2 = my_cons('c', None)
p1 = my_cons('b', p2)
p0 = my_cons('a', p1)
print(f"list starts at index: {p0}")
print(f"car[0]={my_car(p0)} car[1]={my_car(p1)} car[2]={my_car(p2)}")
print(f"cdr[0]={my_cdr(p0)} cdr[1]={my_cdr(p1)} cdr[2]={my_cdr(p2)}")
Representing lists using vectors
A list is a chain of pairs where each cdr points to the next pair (or to the empty list). Walking the list means following cdr pointers through the vector. This is exactly how linked lists work in hardware — each “next” field is a memory address.
Scheme
; Walk a list stored in vector memory.
(define the-cars (make-vector 10 'free))
(define the-cdrs (make-vector 10 'free))
(define free 0)
(define (alloc! car-val cdr-val)
(let ((i free))
(set! free (+ free 1))
(vector-set! the-cars i car-val)
(vector-set! the-cdrs i cdr-val)
i))
; Build (10 20 30 40)
(define p3 (alloc! 40 'nil))
(define p2 (alloc! 30 p3))
(define p1 (alloc! 20 p2))
(define p0 (alloc! 10 p1))
; Walk the list following cdr pointers
(define (walk ptr)
(if (eq? ptr 'nil)
(display "nil")
(begin
(display (vector-ref the-cars ptr))
(display " -> ")
(walk (vector-ref the-cdrs ptr)))))
(display "list: ") (walk p0) (newline)
; Show the raw memory layout
(display "index: ")
(let loop ((i 0))
(if (< i free)
(begin (display i) (display " ") (loop (+ i 1)))))
(newline)
(display "cars: ")
(let loop ((i 0))
(if (< i free)
(begin (display (vector-ref the-cars i)) (display " ") (loop (+ i 1)))))
Python
# Walk a list stored in vector memory
the_cars = [None] * 10
the_cdrs = [None] * 10
free = 0def alloc(car_val, cdr_val):
global free
i = free
free += 1
the_cars[i] = car_val
the_cdrs[i] = cdr_val
return i
p3 = alloc(40, 'nil')
p2 = alloc(30, p3)
p1 = alloc(20, p2)
p0 = alloc(10, p1)
# Walk following cdr pointersdef walk(ptr):
parts = []
while ptr != 'nil':
parts.append(str(the_cars[ptr]))
ptr = the_cdrs[ptr]
return' -> '.join(parts) + ' -> nil'print(f"list: {walk(p0)}")
print(f"index: {list(range(free))}")
print(f"cars: {the_cars[:free]}")
Stop-and-copy garbage collector
When free memory runs out, the collector copies all reachable data from “from-space” to “to-space,” then flips. Dead data stays behind. The copy is contiguous — fragmentation vanishes. The cost is proportional to live data, not total memory.
# Stop-and-copy GC simulation
SIZE = 5
from_cars = ['free'] * SIZE
from_cdrs = ['free'] * SIZE
to_cars = ['free'] * SIZE
to_cdrs = ['free'] * SIZE
# Allocate 5 cells
cells = [('dead1','nil'), ('alive1','nil'), ('dead2','nil'),
('alive2', 1), ('dead3','nil')]
for i, (car, cdr) inenumerate(cells):
from_cars[i] = car
from_cdrs[i] = cdr
print(f"Before GC โ from-space full (5/{SIZE})")
# Root is index 3 (alive2 -> alive1)
free = 0
forwarded = {}
def copy_cell(old_idx):
global free
if old_idx in forwarded:
return forwarded[old_idx]
new_idx = free
free += 1
to_cars[new_idx] = from_cars[old_idx]
to_cdrs[new_idx] = from_cdrs[old_idx]
forwarded[old_idx] = new_idx
# Recursively copy cdr if it's a pointerifisinstance(to_cdrs[new_idx], int):
to_cdrs[new_idx] = copy_cell(to_cdrs[new_idx])
return new_idx
new_root = copy_cell(3)
print(f"After GC โ to-space has {free} live cells")
print(f"to_cars: {to_cars[:free]}")
print(f"3 dead cells reclaimed")
The broken-heart algorithm
When a cell is copied to new space, a broken heart (forwarding pointer) is left in old space. If the collector encounters this cell again via another reference, it follows the forwarding pointer instead of copying a second time. This handles shared structure and cycles.
An alternative to stop-and-copy is a free list: link all unused cells into a chain, allocate from the head. Simpler but causes fragmentation. Stop-and-copy trades the cost of moving live data for the benefit of contiguous allocation — every allocation is a pointer bump.
Scheme
; Free-list allocator: simple but fragmenting.
(define SIZE 8)
(define memory (make-vector SIZE 'free))
; Build free list: 0 -> 1 -> 2 -> ... -> 7 -> nil
(define free-list
(let loop ((i (- SIZE 1)) (next 'nil))
(if (< i 0) next
(loop (- i 1) (cons i next)))))
(define (alloc-free-list! val)
(if (null? free-list)
(error "Out of memory!")
(let ((idx (car free-list)))
(set! free-list (cdr free-list))
(vector-set! memory idx val)
idx)))
(define (dealloc! idx)
(vector-set! memory idx 'free)
(set! free-list (cons idx free-list)))
(define a (alloc-free-list! 'A))
(define b (alloc-free-list! 'B))
(define c (alloc-free-list! 'C))
(display "Allocated A=") (display a)
(display " B=") (display b)
(display " C=") (display c) (newline)
; Free B โ creates a hole
(dealloc! b)
(display "After freeing B, memory: ")
(let loop ((i 0))
(if (< i SIZE)
(begin (display (vector-ref memory i)) (display " ")
(loop (+ i 1)))))
(newline)
(display "Free list head: ") (display (car free-list))
(display " (fragmented!)")
Python hides all of this behind the runtime. sys.getrefcount() exposes reference counts; gc.collect() triggers the cycle collector. CPython uses reference counting (immediate collection) plus a generational collector for cycles — not stop-and-copy. The SICP model is closer to what the JVM or Go runtime does with copying/compacting collectors. The vector representation of pairs maps directly to how struct-of-arrays layouts work in systems programming.
Neighbors
Other reading pages
SICP Ch.19 — Register Machine Simulator (the substrate this chapter manages memory for)