Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • How to Use This Course
  • Reading Path
  • Cache Coherency Glossary
  • The Shared Course Model
  • Cache Coherency Revision Sheet
  • Why Coherence Exists8
  • States and Protocols12
  • Transactions12
  • Races and Controller Design8
  • Progress and the Fabric16
  • Ordering and Synchronization12
  • Performance and Scalability12
  • Real Protocols12
  • SoC Integration12
    • LLC Data and Directory Policies
    • Devices and DMA
    • The Memory-Controller Boundary
      • Point of Coherence and Serialization
      • Memory-Controller Reordering
      • Same-Address Hazards Below the Home
      • Memory Latency and Coherence
  • Power, Clocking and Reliability12
  • Verification12
  • Debug and Bring-up12
  • Interview and Design Review17
  • Capstone: A Coherent Model Lab5
Loading lesson

‹The Memory-Controller Boundary
  1. Topics
  2. Cache Coherency
  3. SoC Integration
  4. The Memory-Controller Boundary
  5. Memory-Controller Reordering

Part 9 · SoC Integration · All levels

Memory-Controller Reordering

Let the memory controller reorder different addresses while a write-queue address check keeps same-address order.

On this page

    Can memory requests finish in a different order?

    Yes, for different addresses. The rule: coherence for X is serialized at H, per line. Below H, either H issues at most one memory access for X at a time and waits for its acceptance (the course default: PutM, H writes memory, Put-Ack, and only then the next request for X), or the controller must keep same-address order for what it has accepted. In practice it checks each read against the write queue by address: on a hit it forwards the youngest queued data or holds the read until the write issues. Different addresses may be reordered freely.

    How a scheduler reorders

    A typical scheduler holds separate read and write queues. It prefers ready row-buffer hits (FR-FCFS: first-ready, first-come-first-served), serves reads before writes, and drains writes in bursts when the write queue passes a high watermark. All of this reorders commands to different addresses. Same-address order is kept by the write-queue address check (forward or block), not by the scheduling policy.

    Where overlapping commands for X come from

    If H waited for DRAM itself before the next access to X, it would never have two memory accesses for X outstanding. They appear as soon as H treats a write as done when the controller's write queue has accepted it (the acceptance rule of the previous lesson), with or without Part 3's early Put-Ack: C0's PutM puts W X = 1 into the controller's write queue and H sends Put-Ack and records U. C1 then takes M (its GetM's memory read must return 1, by the same check), writes 2 and evicts, so H accepts W X = 2. A later GetS makes H issue R X. The controller now holds W X = 1, W X = 2 and R X for one line at once, alongside unrelated work such as W Y = 9 (Y is a different line).

    Worked read-after-write case, keeping that trace's roles: W X = 1 sits in the write queue, C1's GetM reaches H, and H issues R X. DRAM still holds 0. With the address check the read hits the queued write and returns 1, so C1 fills with 1 and H records exclusive(C1). Without the check the scheduler serves R X from DRAM ahead of the queued write: the read returns 0 while H records exclusive(C1), a stale fill that no cache-state check will notice.

    The checker below models the controller. All four commands are accepted in order (W X = first, W Y = 9, W X = second, R X) and each order is the order in which they issue to DRAM. A write waits in the write queue until it issues. A read returns the youngest older queued write to its address if there is one, otherwise DRAM. Assume, for this checker only, that commands are accepted in index order. An order is legal when it keeps write-write order per address. Reads may issue early because the check forwards to them. The test enumerates all 24 orders, asserts that exactly the legal ones give the expected result, and keeps two negative cases: an unchecked read that issues before the write drains returns a stale value, and a write-write inversion leaves X equal to the first value.

    example.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    from itertools import permutations
    def legal(order, commands):
        pos = {c: i for i, c in enumerate(order)}
        return all(pos[i] < pos[j]
                   for i in range(len(commands)) for j in range(i + 1, len(commands))
                   if commands[i][0] == commands[j][0] == "W"
                   and commands[i][1] == commands[j][1])
    def execute(order, commands, check=True):
        # All commands are accepted in index order. order is the order in which
        # the scheduler issues them to DRAM. A write waits in the write queue
        # until it issues. A read is served when it issues.
        dram, queue, reads = {"X": 0, "Y": 0}, set(range(len(commands))), []
        for i in order:
            op, line, value = commands[i]
            queue.discard(i)
            if op == "W":
                dram[line] = value
                continue
            older = [j for j in queue if j < i and commands[j][:2] == ("W", line)]
            if check and older:
                reads.append(commands[max(older)][2])  # youngest older write
            else:
                reads.append(dram[line])
        return dram, reads
    for first, second in ((1, 2), (7, 11)):
        commands = [("W", "X", first), ("W", "Y", 9),
                    ("W", "X", second), ("R", "X", None)]
        expected = ({"X": second, "Y": 9}, [second])
        assert execute(range(4), commands) == expected
        orders = list(permutations(range(4)))
        assert len(orders) == 24
        for o in orders:
            assert legal(o, commands) == (execute(o, commands) == expected), o
        assert sum(legal(o, commands) for o in orders) == 12
        # Without the queue check, a read issued before W X = second drains is stale.
        early = [o for o in orders if legal(o, commands) and o.index(3) < o.index(2)]
        assert early and all(execute(o, commands, check=False)[1] != [second]
                             for o in early)
        assert execute((3, 0, 1, 2), commands, check=False)[1] == [0]
        # A write-write inversion leaves X == first, even with forwarding.
        for o in orders:
            if not legal(o, commands):
                assert execute(o, commands)[0]["X"] == first
    # The worked read-after-write case: W X = 1 queued, R X issues first.
    raw = [("W", "X", 1), ("R", "X", None)]
    assert execute((1, 0), raw) == ({"X": 1, "Y": 0}, [1])
    assert execute((1, 0), raw, check=False)[1] == [0]
    print("PASS: 12 of 24 orders legal, exactly those correct, unchecked read stale")

    Command pair

    Scheduler may

    Kept by

    W Y = 9 and anything for X

    Issue in either order

    Nothing needed: different addresses.

    W X = 1 then R X

    Issue R X first

    Address check: R X returns the queued 1, never DRAM's 0.

    W X = 1 then W X = 2

    Never issue W X = 2 first

    Write queue keeps same-address write order (or merges, keeping the youngest).

    R X then a younger W X

    Never let the younger write reach the read's data first

    Older read issues first, or takes its data before the write drains.

    A read checks the write queue by address and either takes forwarded data or waits. A write never passes an older write to the same address.
    Different addresses move freely. The address check, not the scheduling policy, keeps same-address order.

    Reordering different lines below that acceptance point is invisible to every observer. It cannot break coherence (per location) or the memory model, provided H or the requester counts a write complete only at acceptance (see Completion Versus Visibility).

    Common pitfall

    Assuming FR-FCFS or read priority keeps same-address order. The policy reorders freely. Only the address check against the write queue makes R X return the queued value.

    Interview

    Give the rule (H serializes per line, the controller keeps same-address order for what it accepted), the structure (write-queue address check, forward or block), the worked stale fill without it, and why reordering different lines is invisible.

    Check yourself The scheduler serves R X before the queued W X = 1 drains. Is that legal?

    Yes, if the read checks the write queue and returns the queued 1. Without the check it returns DRAM's 0 while H records C1 as a holder: a stale fill.

    Related topics

    • Shared course model

    • Default order and early Put-Ack

    • Match obligations and responses

    Previous

    107 / 167

    Next: Same-Address Hazards Below the Home

    Up next

    Up nextSame-Address Hazards Below the Home

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Memory Latency and Coherence