False Sharing and Structure Padding in C++

False sharing is a critical performance degradation issue in multi-threaded C++ applications that occurs when independent threads modify distinct variables that reside within the same CPU cache line. Although the threads operate on logically separate data, the underlying hardware cache coherency protocols treat the entire cache line as a single unit, repeatedly invalidating it across CPU cores. This article explains the mechanics of false sharing, the performance costs associated with cache line bouncing, and how to effectively eliminate the problem in C++ using structure padding and modern memory alignment techniques.

The Mechanics of False Sharing

Modern processors read from and write to system memory in fixed-size blocks known as cache lines, which are typically 64 bytes wide. When a core needs to modify a variable, it loads the entire cache line containing that variable into its local L1/L2 cache and marks it as exclusive or modified using a cache coherency protocol (such as MESI or MOESI).

False sharing occurs under the following conditions:

  1. Two or more threads running on separate CPU cores access distinct variables.
  2. These independent variables are located close to each other in memory, falling into the same 64-byte cache line.
  3. At least one of the threads writes to its variable.

When Core 1 writes to Variable A, the cache coherency protocol invalidates the entire cache line in Core 2's cache, even if Core 2 only cares about Variable B. When Core 2 attempts to read or write Variable B, it suffers a cache miss, forcing it to fetch the updated line from Core 1 or main memory. This continuous invalidation and reloading cycle is called "cache line bouncing" or "cache sloshing," and it can severely degrade multi-threaded scaling.

Consider this problematic structure:

struct Metrics {
    uint64_t thread_one_count; // 8 bytes
    uint64_t thread_two_count; // 8 bytes
};

Because both 8-byte integers sit adjacent in memory, they occupy the same 64-byte cache line. If Thread 1 frequently updates thread_one_count while Thread 2 updates thread_two_count, neither thread can retain exclusive cache access, leading to severe latency spikes.

Preventing Invalidation with Structure Padding

Structure padding prevents false sharing by manually or automatically introducing unused space between variables. By expanding the offset between variables to at least the size of a cache line (64 bytes on most modern x86 and ARM architectures), each variable is forced onto its own independent cache line.

A manual approach involves inserting an explicit byte array between fields:

struct PaddedMetrics {
    uint64_t thread_one_count;
    char padding[56];          // 8 + 56 = 64 bytes (1 full cache line)
    uint64_t thread_two_count;
};

In this layout, thread_two_count is shifted onto a new cache line, isolating Core 1's write operations from Core 2's cache.

The Modern C++ Approach: alignas

While manual byte padding works, it is error-prone, platform-dependent, and sensitive to structure reordering. Modern C++ provides cleaner, compiler-enforced mechanisms to control alignment.

C++11 introduced the alignas specifier, which forces the compiler to allocate a member or type at an address that is a multiple of the specified boundary:

struct AlignedMetrics {
    alignas(64) uint64_t thread_one_count;
    alignas(64) uint64_t thread_two_count;
};

With alignas(64), the compiler guarantees that thread_two_count starts on a 64-byte boundary, effectively creating internal padding and ensuring the two variables never share a cache line.

Utilizing C++17 Hardware Interference Constants

Hardcoding 64 assumes standard desktop architectures, but cache lines can vary across systems. C++17 standardized this via <new> with std::hardware_destructive_interference_size:

#include <new>
#include <cstdint>

struct PortableMetrics {
    alignas(std::hardware_destructive_interference_size) uint64_t thread_one_count;
    alignas(std::hardware_destructive_interference_size) uint64_t thread_two_count;
};

std::hardware_destructive_interference_size reports the minimum byte offset required to avoid false sharing on the target architecture at compile time.

By applying alignment or padding to thread-hot data members, you ensure that writes to one variable do not trigger cross-thread cache line invalidations, allowing parallel threads to achieve linear performance scaling.