From Correct to Fast: Optimizing Atomics with Cache Line Alignment
Atomics are safe, but not always fast — especially when multiple threads fight over the same cache line.
From Correct to Fast: Optimizing Atomics with Cache Line Alignment
Atomics are safe, but not always fast — especially when multiple threads fight over the same cache line.
What is an atomic?
In C++, std::atomic<T> provides thread-safe operations on a variable without using mutexes.
- The operation is atomic, meaning no other thread can see a partially updated value.
std::atomic<int> counter;
counter.fetch_add(1); // thread-safe increment
std::atomic<int>guarantees correctness across threads, often via hardware instructions likelock xaddon x86 CPU, which is hardware-enforced atomicity. For assembly-level insight, check this Godbolt example.
Cache line:
CPUs do not read or write memory byte by byte. Instead they work with cache lines, typically 64 bytes.
Data is transferred between memory and cache in blocks of fixed size, called cache lines or cache blocks.
- When it writes/reads, it locks or invalidates the entire cache line across cores for atomic operations.
- This is why two variables in the same cache line can interfere, even if they’re logically unrelated.
This interference is called false sharing.
Benchmark: non-padded vs padded atomics
We can illustrate the impact of false sharing with a simple benchmark.
- Non-padded atomic array — likely all elements share the same cache line.
- Padded atomic array — each element gets its own 64-byte cache line.
#include <atomic>
#include <thread>
#include <vector>
#include <chrono>
#include <iostream>
constexpr int ITER = 10'000'000;
constexpr int THREADS = 4;
// Non-padded
std::atomic<int> arr[THREADS];
// Padded (64-byte alignment)
struct alignas(64) PaddedAtomic { std::atomic<int> value; };
PaddedAtomic paddedArr[THREADS];
void worker(std::atomic<int>* a){ for(int i=0;i<ITER;i++) a->fetch_add(1); }
void worker_padded(PaddedAtomic* a){ for(int i=0;i<ITER;i++) a->value.fetch_add(1); }
int main() {
std::vector<std::thread> threads;
auto start = std::chrono::high_resolution_clock::now();
for(int i=0;i<THREADS;i++) threads.emplace_back(worker, &arr[0]); // All threads increment the same atomic -> causes false sharing
for(auto& t: threads) t.join();
auto end = std::chrono::high_resolution_clock::now();
std::cout << "Non-padded time: " << std::chrono::duration<double>(end-start).count() << " s\n";
threads.clear();
start = std::chrono::high_resolution_clock::now();
for(int i=0;i<THREADS;i++) threads.emplace_back(worker_padded, &paddedArr[i]);
for(auto& t: threads) t.join();
end = std::chrono::high_resolution_clock::now();
std::cout << "Padded time: " << std::chrono::duration<double>(end-start).count() << " s\n";
}

Results on i7–12700K (20CPUS)
That’s a 7x speedup just by avoiding false sharing.
Before (shared cache line): [arr0][arr1][arr2] … <- contention between threads
After (padded): [arr0………][arr1………][arr2………] <- each thread updates its own line
Key Takeaways:
- Atomic operations guarantee correctness but are not free.
- CPUs enforce cache-line-level exclusivity, so layout matters.
- False sharing can slow down unrelated variables in the same line.
- Use padding (
alignas(64)) for frequently updated atomics in multithreaded code.
(Note: Illustrations are added with the help of godbolt.org and AI)
메타데이터
- post_id
- fc59dbe47269
- slug
- from-correct-to-fast-optimizing-atomics-with-cache-line-alignment-fc59dbe47269
- url
- https://medium.com/@bethe1tweets/from-correct-to-fast-optimizing-atomics-with-cache-line-alignment-fc59dbe47269
- canonical_url
- https://medium.com/@bethe1tweets/from-correct-to-fast-optimizing-atomics-with-cache-line-alignment-fc59dbe47269
- author_url
- https://medium.com/@bethe1tweets
- status
- ok
- fetched_at
- 2026-06-26 12:24:55