C++ compiler
thread safety
memory locations
concurrency
multi-threading

What does the C compiler do to ensure that different but adjacent memory locations are safe to be used on different threads?

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

The C and C++ standards guarantee that distinct memory locations can be safely accessed by different threads without synchronization, even if they are adjacent. The compiler ensures this by never generating code that writes to a memory location it was not asked to write to. Specifically, the compiler must not use read-modify-write sequences on a wider memory unit (like a full word) when the program only modifies a single byte or bitfield within it. This constraint was formalized in C11 and C++11 and is commonly known as the "no spurious writes" or "no adjacent memory clobbering" guarantee.

The Problem: Adjacent Memory and False Sharing

c
1struct Data {
2    char a;  // Thread 1 writes this
3    char b;  // Thread 2 writes this
4};

a and b are adjacent bytes. On most architectures, they share the same cache line and possibly the same machine word. A naive compiler might optimize a write to a by loading the entire 4-byte or 8-byte word, modifying the a byte, and writing the word back. But this read-modify-write clobbers b — a catastrophic data race if thread 2 is simultaneously writing to b.

What the Standard Requires

C11 (section 3.14) and C++11 (section 1.7) define a "memory location" as either a scalar object or a maximal sequence of adjacent bitfields with non-zero width. The rule is:

  • Two threads can access different memory locations concurrently without synchronization.
  • The implementation must not introduce writes to memory locations the program does not write to.
c
1// The compiler MUST NOT generate code like this:
2// To write a = 5:
3//   temp = *(int*)&data;     // Load 4 bytes (includes b)
4//   temp = (temp & 0xFFFFFF00) | 5;
5//   *(int*)&data = temp;     // Writes back b too — DATA RACE
6
7// The compiler MUST generate something like:
8//   store_byte(&data.a, 5);  // Only touches a

How Compilers Implement This

Byte-Addressable Stores

On x86, ARM, and most modern architectures, the hardware supports byte-granularity stores. The compiler simply emits a single-byte store instruction:

asm
1; x86: writing a single byte
2mov byte ptr [rdi], 5    ; Stores only 1 byte, adjacent bytes untouched
3
4; ARM: writing a single byte
5strb w0, [x1]            ; Byte store — does not touch adjacent memory

Bitfields: The Difficult Case

c
1struct Flags {
2    unsigned int x : 4;  // Thread 1
3    unsigned int y : 4;  // Thread 2 — SAME memory location!
4};

Adjacent bitfields within the same underlying storage unit share a memory location. The compiler typically loads the entire word, modifies the target bits, and stores the word back. This means adjacent bitfields in the same storage unit cannot be safely accessed from different threads.

c
1struct SafeFlags {
2    unsigned int x : 4;
3    unsigned int : 0;     // Zero-width bitfield forces a new storage unit
4    unsigned int y : 4;   // Now in a separate memory location — thread-safe
5};

The zero-width bitfield tells the compiler to start a new storage unit, placing y in a different memory location that can be independently accessed.

Alpha Architecture: The Historic Exception

The DEC Alpha processor lacked byte-granularity stores — it could only write 4-byte or 8-byte words. To write a single byte, the compiler had to use a load-locked/store-conditional (LL/SC) sequence:

asm
1; Alpha: atomic byte write using LL/SC
2retry:
3    ldl_l  t0, 0(a0)      ; Load-locked the containing word
4    insbl  a1, a0, t1     ; Insert byte into position
5    mskbl  t0, a0, t0     ; Mask out the old byte
6    or     t0, t1, t0     ; Combine
7    stl_c  t0, 0(a0)      ; Store-conditional — fails if another thread modified the word
8    beq    t0, retry       ; Retry if store-conditional failed

This LL/SC loop ensures that if another thread modifies an adjacent byte in the same word between the load and store, the store fails and retries. Modern architectures (x86, ARM, RISC-V) avoid this by supporting native byte stores.

Practical Implications

c
1#include <stdatomic.h>
2#include <pthread.h>
3
4struct SharedData {
5    int counter;       // Thread 1 modifies
6    int flag;          // Thread 2 modifies — adjacent but distinct location
7};
8
9// Safe: different memory locations, no synchronization needed
10void* thread1(void* arg) {
11    struct SharedData* d = arg;
12    d->counter = 42;   // Compiler emits a store to &d->counter only
13    return NULL;
14}
15
16void* thread2(void* arg) {
17    struct SharedData* d = arg;
18    d->flag = 1;       // Compiler emits a store to &d->flag only
19    return NULL;
20}

This is safe by the standard. However, while there is no data race, there may be a performance issue: both fields share the same cache line, causing "false sharing" — the cache coherency protocol bounces the cache line between cores on every write.

c
1// Avoid false sharing with padding or alignment
2struct PaddedData {
3    alignas(64) int counter;  // Own cache line
4    alignas(64) int flag;     // Own cache line
5};

Common Pitfalls

  • Assuming adjacent bitfields are separate memory locations: Bitfields in the same underlying storage unit are a single memory location. Two threads writing to adjacent bitfields without synchronization is a data race, even though they appear to be separate fields. Use zero-width bitfields or separate integer fields to force distinct storage units.
  • Confusing thread safety with cache performance: The compiler guarantees correctness (no spurious writes) but not performance. Two threads writing to adjacent fields on the same cache line cause false sharing — the cache line ping-pongs between cores, severely degrading throughput. Use alignas(64) or padding to put hot fields on separate cache lines.
  • Relying on compiler-specific behavior for older architectures: The Alpha architecture required LL/SC sequences for byte stores. Code compiled without proper C11/C++11 support on such architectures may generate word-level stores that clobber adjacent bytes. Always compile with -std=c11 or -std=c++11 or later to get the memory model guarantees.
  • Using volatile instead of atomics for shared data: volatile prevents compiler reordering but does not provide atomicity or memory ordering guarantees. For data shared between threads, use _Atomic (C11) or std::atomic (C++11). volatile is for hardware registers, not thread synchronization.
  • Assuming struct packing is portable: #pragma pack(1) removes padding, placing fields at byte boundaries. While this does not create data races (byte stores are safe on modern hardware), it can cause unaligned access penalties and breaks alignas directives intended to prevent false sharing. Use packing only for serialization, not for thread-shared structures.

Summary

  • C11/C++11 guarantee that writes to distinct memory locations are thread-safe without synchronization
  • Compilers must not generate word-level read-modify-write sequences for byte or short writes
  • Adjacent bitfields in the same storage unit share a memory location — use zero-width bitfields to separate them
  • Modern hardware (x86, ARM) supports native byte stores; the Alpha required LL/SC workarounds
  • False sharing (not data races) is the practical concern for adjacent fields — use alignas(64) to separate hot fields onto different cache lines

Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Basis Lab
No course covers this one yet

Basis Lab writes one for you from a sentence about what you want to be able to do, then teaches it and checks you understood. Your first course is free.

Build my course
Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions