Foundations to FrontierPart IV: Algorithms and systemsBezhig

W03: From program objects to memory behavior

5 exercises, each with a complete solution. 1 figure. 2 code listings. 1 note from the engine.

On this page
  1. Prerequisites and observable goals
  2. The central question: which layer makes the promise?
  3. A lifetime failure that a plausible address can hide
  4. Virtual addresses are not physical addresses
  5. A cache model exposes locality and conflict
  6. Publication: visibility is not just unique work assignment
  7. Performance is a hypothesis about a valid program
  8. Independent practice
  9. Complete solutions: W03
  10. Retention and readiness

Prerequisites and observable goals#

Before

C03 and C09–C10: references, lifetimes, binary representation, caches, virtual memory, and synchronization.

After

Separate language guarantees from a hardware model; reason about ownership and publication; predict small address-translation and cache traces before measuring real programs.

The central question: which layer makes the promise?#

A source program names objects. A compiler selects representations and instructions. An operating system maps virtual memory and schedules threads. A processor moves data through caches and executes instructions under its architectural rules. These layers interact, but a fact from one is not automatically a guarantee from another. “The address looks the same” does not establish that an object is still alive. “The processor can load this width” does not make a compound increment atomic in the language.

Begin a memory argument with three questions. Who owns the object? How long does it live? Which aliases may access or mutate it? Only then ask where bytes are placed and how quickly they can be accessed. A diagram with rectangles and arrows is a representation of those relationships, not a literal view of every compiler’s stack layout.

A lifetime failure that a plausible address can hide#

Consider a C++ vector and a reference to its first element:

C++
std::vector<int> values{10, 20};
int& first = values[0];
// Suppose a later push_back triggers reallocation.
values.push_back(30);
// Using first is now invalid if that reallocation occurred.

The reference designates an element object in the old allocation. A reallocation constructs elements in new storage and invalidates references into the former storage. It does not redirect the existing reference to the new first element. Whether this particular insertion reallocates depends on capacity; do not infer the capacity from an initializer’s length. The conditional wording is essential.

This is an ownership schematic; actual addresses, element movement, and allocation details are implementation-dependent. Keeping an index and reacquiring a reference after mutation can solve this particular problem if the element identity and index remain valid. An index can also become stale after erasure or reordering. Reserve capacity only when you can establish a sufficient bound; it is not a general lifetime proof. [CPP]

Virtual addresses are not physical addresses#

In a simplified paged model with page size PP, split a nonnegative virtual byte address aa as

a=P⌊a/P⌋+(a mod P).a=P\lfloor a/P\rfloor+(a\bmod P).

The quotient is the virtual page number and the remainder is the byte offset. A page table maps the virtual page to a physical frame number FF, giving physical address FP+(a mod P)FP+(a\bmod P). Translation preserves the offset within a page; it need not preserve adjacency between different pages.

For page size 4096 bytes and virtual address 0x2345, page number is 2 and offset 0x345=837. If page 2 maps to frame 9, the physical address is 0x9345. The hexadecimal decomposition works especially neatly because the page size is a power of 16. An unmapped page means this toy translation is unavailable; a real OS may fault, allocate, reject permission, or take another documented action. Our model has no permissions, translation cache, demand paging, or multi-level page tables. It teaches the decomposition, not the full operating system.

A cache model exposes locality and conflict#

For cache-line size BB bytes, memory block number is b=⌊a/B⌋b=\lfloor a/B\rfloor. In a direct-mapped cache of LL lines, line index is b mod Lb\bmod L and tag is ⌊b/L⌋\lfloor b/L\rfloor. A valid line with the matching tag produces a hit; otherwise the access replaces that line and is a miss. This model assumes an initially empty read cache and ignores timing, coherence, stores, prefetching, associativity, and replacement choices beyond direct mapping.

Take B=32,L=2B=32,L=2 and addresses 0,8,32,64,00,8,32,64,0. Blocks are 0,0,1,2,00,0,1,2,0 and indices 0,0,1,0,00,0,1,0,0. Access 0 misses, 8 hits the already loaded block, 32 misses into the other line, 64 conflicts with block 0, and the final 0 misses because block 2 displaced it. A tiny working set can still suffer conflicts. A real associative cache may behave differently; measure actual programs only after validating outputs and recording the hardware/workload.

The connection to structure-of-arrays versus array-of-structures is now concrete. Reading just one field from many records may transfer unrelated fields too. But if a computation uses all fields together, separating them can add streams and index work. A layout recommendation needs an access pattern, element sizes, compiler behavior, and measurement. There is no unconditional theorem that one fashionable layout is always faster.

Publication: visibility is not just unique work assignment#

Suppose workers read an immutable array and each writes a different preallocated result element. Construct the input and results before starting threads; never resize the result vector while they run; join all workers before reading outputs. These choices establish lifetimes and disjoint ownership. An atomic counter can hand out distinct indices with relaxed ordering because it allocates work, not because it publishes changing payload data. Thread creation and joining supply the relevant synchronization for that design.

Now change the design: a producer writes a payload and sets a ready flag while a consumer waits. Merely changing the flag to an atomic does not automatically synchronize the non-atomic payload under every memory order. A release store paired with an acquire load that observes it can establish a happens-before relationship for preceding writes. Both the matching observation and object lifetime matter. A relaxed flag alone supplies atomic flag operations, not publication of the payload. Tests that happen to pass on one machine cannot replace the language memory-model argument. [CPP]

Performance is a hypothesis about a valid program#

First establish that the measured programs implement the same contract and have defined behavior. Then isolate the variable being changed: layout, allocation, tiling, batching, or scheduling. Record input distribution, warmup, repetitions, setup inclusion, build flags, and output checks. A measurement confounded by a different algorithm, smaller output, or invalid pointer cannot support the intended layout claim. Advanced G06 returns to arithmetic intensity and scaling after these model boundaries are clear.

Independent practice#

F foundation C standard course work T transfer S optional stretch L runnable lab or project. Attempt the complete set before opening its solutions.

W03.01TSolution

A program stores a pointer to a vector element, then appends many elements and uses the pointer. A review says, “It printed the right value, so the pointer is safe.” Give the missing conditions, a safe redesign under stable-index assumptions, and a case where that redesign also needs revision.

W03.02CSolution

With page size 4096, translate virtual addresses 0x1FFF and 0x2000 when virtual page 1 maps to frame 7 and page 2 maps to frame 3. Explain whether consecutive virtual bytes must be consecutive physical bytes.

W03.03CSolution

Trace an initially empty direct-mapped cache with 16-byte lines and two lines for byte addresses 0,4,16,32,0,200,4,16,32,0,20. Give block, line, and hit/miss for each access; explain the final access.

W03.04TSolution

A producer writes a non-atomic payload, then stores true to an atomic flag with relaxed ordering. A consumer spins on relaxed loads and reads the payload after seeing true. Explain what the flag guarantees, what is missing for publication, and a sufficient synchronization pattern under a single-producer/single-consumer, valid-lifetime model.

W03.05LSolution

Implement the page translation and direct-mapped cache models in integration_code/starters/memory_models.py. Validate nonnegative integer addresses, page/frame mappings, and positive sizes. Return a translation or a clear missing-map error; return hit/miss results for the cache. Write hand-traced tests, and explicitly list at least four real-machine features absent from the models.

Optional small hints

For .01, ask whether an operation invalidated the designated object, not whether bytes still resemble the old value. For .02, split page and offset. For .04, atomicity and ordering are separate questions.

Complete solutions: W03#

Compare the first step where your work differs. A corrected answer is not yet an independent success.

W03.01Show solution

A successful access is not a lifetime proof

The review lacks a guarantee that capacity was sufficient to avoid reallocation and that no erasure or other invalidating operation occurred. A plausible printed value can come from storage whose object lifetime ended; that is not evidence of valid access. Under a specification preserving order and element identity, store an index and reacquire values[index] after appends, checking bounds. If erasure, sorting, or compaction changes indices, use stable handles with generation checking or an ownership structure matching the needed identity. Merely switching to pointers or reserving an arbitrary capacity does not prove the revised contract.

Back to W03.01
W03.02Show solution

Translation preserves only the within-page offset

0x1FFF has page 1 and offset 0xFFF, so the physical address is 0x7FFF. 0x2000 has page 2 and offset zero, so it maps to 0x3000. The virtual addresses differ by one, but their physical frames are not consecutive. Within each mapped page consecutive offsets remain consecutive; crossing a page boundary requires a new mapping. Real permission and fault behavior is beyond this toy calculation.

Back to W03.02
W03.03Show solution

Repeated data can be displaced by a conflict

Blocks are 0,0,1,2,0,10,0,1,2,0,1; lines are 0,0,1,0,0,10,0,1,0,0,1. Outcomes are miss, hit, miss, miss, miss, hit. Block 2 replaces block 0 in line 0, so revisiting 0 misses. Block 1 stayed in line 1, and address 20 lies in that block, so the final access hits. This trace counts model hits, not nanoseconds; it supplies no measured claim about an actual processor.

Back to W03.03
W03.04Show solution

A matching synchronization relation is needed

The flag’s individual loads/stores are atomic, but relaxed ordering alone does not establish publication of the ordinary payload. In the stated model, write the payload, then perform a release store to the flag. The consumer uses acquire loads and reads the payload only after an acquire load observes that release (or an applicable release sequence under the language rules). That establishes the needed ordering of the preceding payload write before the read. Ensure the payload remains alive and is not concurrently modified again without another protocol. A mutex/condition-variable design can be clearer for a blocking workflow. An aligned integer or one passing run does not supply the missing relationship.

Back to W03.04
W03.05Show solution

Reference models and their boundary

The translation separates quotient and remainder and consults an explicit map rather than treating virtual page number as physical frame number. The cache tracks tags by line index and begins invalid; it does not prefill with tag zero, which would falsely report a first hit for block zero.

integration_code/solutions/memory_models.pyPython
"""W03: schematic address translation and a direct-mapped cache, not hardware timing."""


def translate(address, page_map, page_size=4096):
    if type(address) is not int or address<0 or type(page_size) is not int or page_size<=0:
        raise ValueError("nonnegative address and positive page size required")
    if any(type(x) is not int or x<0 for pair in page_map.items() for x in pair):
        raise ValueError("page numbers must be nonnegative integers")
    page,offset=divmod(address,page_size)
    if page not in page_map:raise KeyError("unmapped virtual page")
    return page_map[page]*page_size+offset


def direct_mapped(addresses,line_size=32,lines=2):
    if any(type(x) is not int or x<=0 for x in (line_size,lines)) or lines>65536:
        raise ValueError("bounded positive cache dimensions required")
    tags=[None]*lines
    result=[]
    for address in addresses:
        if type(address) is not int or address<0:raise ValueError("invalid address")
        block=address//line_size
        index=block%lines
        tag=block//lines
        hit=tags[index]==tag
        tags[index]=tag
        result.append({'address':address,'line':index,'tag':tag,'hit':hit})
    return result


if __name__=='__main__':
    print(translate(0x2345,{2:7}))
    print(direct_mapped([0,64,0,64]))

Tests cover exact boundaries, missing pages, repeated addresses, and conflicts. Missing features include access permissions, TLBs, multi-level caches, write policies, set associativity, speculative execution, coherence, and actual latency. Naming these omissions protects the meaning of a result: the model verifies arithmetic and state transitions, not hardware performance or operating-system policy.

Back to W03.05

Retention and readiness#

Draw the owner, alias, and lifetime before discussing addresses. Explain a cache conflict without using “slow memory” as a substitute for the mapping. For concurrency, identify the exact synchronizes-with or join relationship, not just the presence of an atomic somewhere in the program.