Fundamentals 32 min read

Understanding CPU Cache Hierarchy, Write‑Back Strategy, and Memory Ordering in Multithreaded Programming

This article explains the structure and operation of modern CPU multi‑level caches, the write‑back caching policy, cache‑coherence mechanisms, atomic operations, and various memory‑ordering models in C++ multithreaded programs, providing detailed concepts, hardware and software solutions, and practical code examples.

Deepin Linux
Deepin Linux
Deepin Linux
Understanding CPU Cache Hierarchy, Write‑Back Strategy, and Memory Ordering in Multithreaded Programming

1. Introduction to CPU Caches

Modern CPUs use a multi‑level cache hierarchy to bridge the speed gap between the processor core and main memory, improving performance by keeping frequently accessed data close to the core.

1.1 Multi‑Level Cache System

L1 cache is the smallest and fastest, located nearest to the core, typically accessed within a few CPU cycles.

L2 cache offers larger capacity with slightly higher latency, acting as an intermediate buffer when L1 misses.

L3 cache provides even larger shared storage for multi‑core processors, enabling cores to share data efficiently.

1.2 Cache Line Structure

A cache line is the basic unit of data transfer between cache and main memory, consisting of a flag, a tag, and a data field.

Flag indicates the line’s state (valid, dirty, etc.).

Tag uniquely identifies the memory address of the line.

Data stores the actual bytes transferred.

When a CPU reads a memory address, it first checks the corresponding cache line; if present and valid, the data is read directly from the cache, avoiding a costly main‑memory access.

2. Write‑Back Policy

The write‑back strategy updates data in the cache first and marks the cache line as “dirty”. The modified line is written back to main memory only when it is evicted or explicitly required, reducing memory traffic and improving overall efficiency.

2.1 Handling Write Requests

If the target line is in cache (hit), the CPU writes the new value and marks the line dirty; no immediate write to memory occurs.

If the line is not present (miss), the CPU allocates a cache line, possibly writing back an existing dirty line before loading the new data, then performs the write.

2.2 Handling Read Requests

If the line is cached and valid, the CPU reads directly from it.

If the line is missing, the CPU loads the line from main memory into the cache before returning the data.

3. Dealing with Cache‑Coherence Issues

3.1 Sources of Inconsistency

When multiple cores modify shared data, delayed write‑back can cause stale values in main memory, leading to coherence problems.

3.2 Solutions

Hardware mechanisms such as bus snooping and cache‑coherence protocols (e.g., MESI) ensure that updates made by one core are visible to others.

Software mechanisms include synchronization primitives (locks, semaphores) and careful data‑access patterns to minimize false sharing.

4. Atomic Operations and Their Relation to Caches

4.1 What Is an Atomic Operation?

An atomic operation executes indivisibly, guaranteeing that concurrent threads see either the whole operation or none of it, which is essential for correct multithreaded behavior.

4.2 Implementation in C++

Hardware support includes instructions like Compare‑And‑Swap (CAS) and Load‑Linked/Store‑Conditional (LL/SC). Software can emulate atomics using locks, though with higher overhead.

Common C++ atomic types (from <atomic> ) and example code:

#include <iostream>
#include <atomic>
#include <thread>

std::atomic_bool flag(false);

void setFlagTrue() {
    flag.store(true);
}

void checkFlag() {
    while (!flag.load()) {}
    std::cout << "Flag is true!" << std::endl;
}

int main() {
    std::thread t1(setFlagTrue);
    std::thread t2(checkFlag);
    t1.join();
    t2.join();
    return 0;
}

Other examples show std::atomic<int> counters, pointer atomics, and size‑type atomics.

5. Memory‑Ordering Issues

5.1 What Is Memory Ordering?

Memory ordering defines the visibility and ordering guarantees of reads and writes to shared variables across threads. Without explicit ordering, compilers and CPUs may reorder operations, causing unexpected results.

5.2 Common Memory‑Order Types (C++)

std::memory_order_relaxed : only guarantees atomicity.

std::memory_order_consume : preserves dependency ordering.

std::memory_order_acquire : prevents later reads/writes from moving before the acquire.

std::memory_order_release : prevents earlier reads/writes from moving after the release.

std::memory_order_acq_rel : combines acquire and release.

std::memory_order_seq_cst : strongest, enforces a single total order.

5.3 Memory Barriers

Memory barriers (fences) force ordering constraints on the hardware and compiler. Example using C++ fences:

#include <iostream>
#include <thread>
#include <atomic>

std::atomic<bool> flag(false);
std::atomic<int> data(0);

void writerThread() {
    data.store(42, std::memory_order_relaxed);
    std::atomic_thread_fence(std::memory_order_release);
    flag.store(true, std::memory_order_relaxed);
}

void readerThread() {
    while (!flag.load(std::memory_order_relaxed)) {}
    std::atomic_thread_fence(std::memory_order_acquire);
    int value = data.load(std::memory_order_relaxed);
    std::cout << "Read value: " << value << std::endl;
}

int main() {
    std::thread t1(writerThread);
    std::thread t2(readerThread);
    t1.join();
    t2.join();
    return 0;
}

This ensures that when the reader sees flag == true , it also sees the updated data value.

6. Case Studies

6.1 Multithreaded Locking

Using std::mutex and std::lock_guard to protect a shared counter:

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;
int sharedData = 0;

void increment() {
    for (int i = 0; i < 1000; ++i) {
        std::lock_guard<std::mutex> lock(mtx);
        ++sharedData;
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    t1.join();
    t2.join();
    std::cout << "Final value: " << sharedData << std::endl;
    return 0;
}

6.2 Memory‑Ordering Example

Four threads write and read atomic flags with relaxed ordering, demonstrating how lack of ordering can lead to nondeterministic results, while using memory_order_seq_cst yields predictable behavior.

6.3 Synchronization Problems

Examples show data races when no synchronization is used and how mutexes eliminate the race, emphasizing the trade‑off between correctness and performance.

C++multithreadingCPU cacheatomic operationsmemory-orderingwrite-back
Deepin Linux
Written by

Deepin Linux

Research areas: Windows & Linux platforms, C/C++ backend development, embedded systems and Linux kernel, etc.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.