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
    • False Sharing
    • Directory Capacity and Sharers
    • Hotspots and Placement
      • Hot Lines and Locks
      • Home Interleaving
      • NUMA and Locality
      • Scaling Trade-Off Exercise
  • Real Protocols12
  • SoC Integration12
  • Power, Clocking and Reliability12
  • Verification12
  • Debug and Bring-up12
  • Interview and Design Review17
  • Capstone: A Coherent Model Lab5
Loading lesson

‹Hotspots and Placement
  1. Topics
  2. Cache Coherency
  3. Performance and Scalability
  4. Hotspots and Placement
  5. NUMA and Locality

Part 7 · Performance and Scalability · All levels

NUMA and Locality

Place a page, and with it the home, then cost local, remote and remote-dirty misses on the course model.

On this page

    Local to which part of a miss?

    NUMA means non-uniform memory access: a miss costs more when it has to cross to another node. In the course model and in classic CC-NUMA, the home of X sits at the memory node that holds X's physical page: the directory sits beside that node's memory controller, so placing the page places the home. Use two nodes, A and B, each with its own memory and home. C0 and C1 are both on node A. The dirty-responsible cache is the third location that matters: when it holds X in M, H must fetch the value from it, wherever the page lives.

    A design may separate the two, for example a directory slice chosen by an address hash while memory channels are interleaved differently. That is an extension: a clean miss then pays the requester-to-home legs and a home-to-memory access as separate costs. This lesson keeps home and memory together.

    Worked latency comparison

    Take Anatomy of a Miss Latency's 3 cycles per in-node leg, 2 for H's lookup and 1 for a cache to handle a forward. Drop its 4-cycle request queue and 1-cycle grant construction, which are the same in every case here. Use 12 for a leg that crosses between nodes and 10 instead of Anatomy's 30 for H's memory read, so that a memory read costs about one remote leg. Use MSI. C0 issues a GetS for X, which started at memory = 0. In the dirty cases C1 has stored X = 1 and holds M / 1, H exclusive(C1), and the read takes the four-hop route of Three-Hop Versus Four-Hop. H's memory update for the dirty read is left off C0's path.

    Case, requester C0 on node A

    Legs on C0's path

    Illustrative cycles

    Local home, clean: X's page on node A, H U

    2 local: GetS, Data

    3 + 2 + 10 + 3 = 18

    Remote home, clean: X's page on node B, H U

    2 remote: GetS, Data

    12 + 2 + 10 + 12 = 36

    Remote home, dirty: page on node B, C1 M / 1

    4 remote: GetS, Fwd-GetS, Data to H, Data to C0

    12 + 2 + 12 + 1 + 12 + 12 = 51

    Same dirty read after the page moves to node A

    4 local

    3 + 2 + 3 + 1 + 3 + 3 = 15

    The remote-dirty case is the CC-NUMA trap: the newest value sits in C1, on C0's own node, yet every leg crosses to the home on node B and back. Moving the page to node A turns 51 cycles into 15, cheaper even than the local clean read, because no memory read is needed. The ordering depends on the leg costs: with uniform 3-cycle legs, both page placements cost the same.

    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
    # Illustrative cycles, not measurements. Legs as in Anatomy of a Miss Latency,
    # simplified: its request queue and grant construction are dropped (equal in
    # every case) and its 30-cycle memory read is cut to 10.
    LOOKUP, HANDLE, MEMORY = 2, 1, 10
    def clean(leg):                     # GetS, H lookup, memory read, Data
        return leg + LOOKUP + MEMORY + leg
    def dirty(to_home, home_to_holder):  # four-hop: GetS, Fwd-GetS, Data to H, Data
        return to_home + LOOKUP + home_to_holder + HANDLE + home_to_holder + to_home
    def cases(local, remote):           # C0 and C1 on node A, requester C0
        return {
            "local-home clean": clean(local),            # X's page on node A
            "remote-home clean": clean(remote),          # X's page on node B
            "remote-home dirty": dirty(remote, remote),  # page on B, C1 holds M
            "migrated dirty": dirty(local, local),       # page moved to node A
        }
    c = cases(local=3, remote=12)
    assert c == {"local-home clean": 18, "remote-home clean": 36,
                 "remote-home dirty": 51, "migrated dirty": 15}
    assert c["local-home clean"] < c["remote-home clean"] < c["remote-home dirty"]
    assert c["migrated dirty"] < c["local-home clean"]  # neighbour beats memory
    u = cases(local=3, remote=3)        # uniform legs: placement stops mattering
    assert u["local-home clean"] == u["remote-home clean"]
    assert u["remote-home dirty"] == u["migrated dirty"]
    print("PASS: remote home 36 and remote dirty 51 cycles versus 18 local, 15 after migration")

    First-touch placement and page migration

    Most operating systems place a page on first touch: the page is allocated from the memory of the node whose core first faults it (Linux's default policy allocates on the faulting CPU's local node). The classic mistake is a single initialisation thread on node B that touches a whole array before the worker threads on node A start: every page, and so every home, lands on node B, and every miss the workers take is a remote case from the table. The fix is to initialise the data with the same threads, on the same nodes, that will later use it, or to bind the memory explicitly.

    Page migration repairs a bad placement later. The OS (for example Linux's automatic NUMA balancing, or an explicit move_pages call) copies the page to a frame on the node that uses it, updates the page table and invalidates the stale TLB entries. The copy reads every line coherently, so a line held dirty in a cache is supplied by its holder, not by stale memory. The new frame has a new physical address and therefore a new home; coherence moves nothing, the old home simply stops receiving requests for that data. A 4 KiB page is 64 lines plus a TLB shootdown, so migration pays off only for a page that will take many more misses at its new node.

    Location question

    Determines

    Does not determine

    Which core requests?

    Path origin and local-cache opportunity

    Where the newest data currently resides.

    Which node holds X's page, and so its home?

    Conflict ordering, the home legs and the backing memory path

    Whether memory is current.

    Which cache is dirty-responsible?

    Required current-data source and the extra two legs

    The best long-term placement for a changing workload.

    A request either hits locally or reaches the home at X's memory node, which reads memory for a clean line or forwards to the private holder of a dirty one.
    Locality is decided by the home's node and the holder's node, along the actual path.

    A placement experiment should keep the logical work constant while changing one policy at a time: page placement, thread affinity or data layout. Record memory traffic separately from cache-to-cache traffic. For a single truly shared X written by cores on both nodes, no placement removes the remote legs for every writer.

    Common pitfall

    Calling every remote access a remote DRAM access. The value may come from another private cache through a remote home.

    Interview

    Draw requester, home node and dirty-responsible holder separately, then cost a miss leg by leg. State which location each proposed optimisation actually moves.

    Check yourself Does first-touch placement prove later dirty data stays near the first toucher?

    No. It fixes where the page, and so the home, lives. Coherent writes can move the current private copy to a core on another node, and then every miss pays the legs to the home and to that holder.

    Check yourself In the table, why does the dirty read on node A's home cost less than a local clean read?

    Both stay inside node A, and the dirty read replaces the 10-cycle memory read with two 3-cycle legs and 1 cycle of handling: 15 against 18 cycles with these illustrative numbers.

    Related topics

    • Shared course model

    • Leg-by-leg latency

    • Data authority before proximity

    Previous

    84 / 167

    Next: Scaling Trade-Off Exercise

    Up next

    Up nextScaling Trade-Off Exercise

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Hot Lines and Locks