What 64 Bytes of Padding Are Worth
How much throughput does false sharing cost when several threads increment counters that share one cache line, compared with the same counters padded onto separate lines?
Results
| Repeats | 5 |
|---|---|
| Threads | 8 |
| Slowdown Factor | 15.39 |
| Padded Best Seconds | 0.0273 |
| Padded Mean Seconds | 0.0291 |
| Iterations Per Thread | 10000000 |
| Shared Line Best Seconds | 0.42 |
| Shared Line Mean Seconds | 0.5119 |
| Padded Million Ops Per Sec | 2931.56 |
| Shared Line Million Ops Per Sec | 190.47 |
Recorded September 8, 2026 at 5:01 AM UTC, wall clock 2.8s.
Method
Eight threads each perform 10,000,000 atomic increments (GCC __sync_fetch_and_add). In the first case the counters are packed 8 bytes apart, so all of them land inside a single 64-byte cache line; in the second each counter is padded to a full line. Nothing else differs: same thread count, same iteration count, same atomic operation, same allocation alignment. Both cases are warmed up at one tenth the workload before measurement to absorb page faults and CPU frequency ramp-up, then each is run five times alternately and reported by its fastest run, so a transient on one case cannot flatter the other.
Machine
| CPU | AMD EPYC 9354P 32-Core Processor |
|---|---|
| Cores visible | 8 |
| Memory | 31.3 GB |
| Kernel | 6.8.0-139-generic |
| Architecture | x64 |
| Compiler | gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0 |
This is a shared virtual server, not an isolated test rig. Absolute throughput will differ on your hardware; the ratio between the two cases is the part that carries over.
Source
The complete program that produced the numbers above. Nothing else was running under our control during the measurement.
/*
* Measures the cost of false sharing: N threads incrementing counters that
* either land on the same cache line or on separate ones.
*
* The only difference between the two runs is 64 bytes of padding. Everything
* else — thread count, iteration count, memory order, the increment itself —
* is identical, so the gap between them is the cache-coherence traffic and
* nothing else.
*
* Build: gcc -O2 -pthread -o bench bench.c
* Run: ./bench <threads> <iterations_per_thread>
* Output: one JSON object on stdout.
*/
#define _GNU_SOURCE
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#define CACHE_LINE 64
typedef struct {
volatile uint64_t value;
} packed_counter_t;
typedef struct {
volatile uint64_t value;
char pad[CACHE_LINE - sizeof(uint64_t)];
} padded_counter_t;
typedef struct {
void *counter;
uint64_t iterations;
int padded;
} worker_arg_t;
static void *worker(void *raw) {
worker_arg_t *arg = (worker_arg_t *)raw;
if (arg->padded) {
padded_counter_t *c = (padded_counter_t *)arg->counter;
for (uint64_t i = 0; i < arg->iterations; i++) __sync_fetch_and_add(&c->value, 1);
} else {
packed_counter_t *c = (packed_counter_t *)arg->counter;
for (uint64_t i = 0; i < arg->iterations; i++) __sync_fetch_and_add(&c->value, 1);
}
return NULL;
}
static double now_seconds(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
}
static double run_case(int threads, uint64_t iterations, int padded) {
void *counters;
size_t stride = padded ? sizeof(padded_counter_t) : sizeof(packed_counter_t);
if (posix_memalign(&counters, CACHE_LINE, stride * (size_t)threads) != 0) {
fprintf(stderr, "allocation failed\n");
exit(1);
}
memset(counters, 0, stride * (size_t)threads);
pthread_t *tids = calloc((size_t)threads, sizeof(pthread_t));
worker_arg_t *args = calloc((size_t)threads, sizeof(worker_arg_t));
double start = now_seconds();
for (int i = 0; i < threads; i++) {
args[i].counter = (char *)counters + stride * (size_t)i;
args[i].iterations = iterations;
args[i].padded = padded;
pthread_create(&tids[i], NULL, worker, &args[i]);
}
for (int i = 0; i < threads; i++) pthread_join(tids[i], NULL);
double elapsed = now_seconds() - start;
free(tids);
free(args);
free(counters);
return elapsed;
}
int main(int argc, char **argv) {
int threads = argc > 1 ? atoi(argv[1]) : 4;
uint64_t iterations = argc > 2 ? strtoull(argv[2], NULL, 10) : 20000000ULL;
/* Warm up so the first measured case does not pay for page faults and
* frequency ramp-up that the second one avoids. */
run_case(threads, iterations / 10, 0);
run_case(threads, iterations / 10, 1);
const int repeats = 5;
double packed_best = 1e9, padded_best = 1e9;
double packed_sum = 0, padded_sum = 0;
for (int r = 0; r < repeats; r++) {
double p = run_case(threads, iterations, 0);
double q = run_case(threads, iterations, 1);
if (p < packed_best) packed_best = p;
if (q < padded_best) padded_best = q;
packed_sum += p;
padded_sum += q;
}
double total_ops = (double)iterations * (double)threads;
printf("{\n");
printf(" \"threads\": %d,\n", threads);
printf(" \"iterations_per_thread\": %llu,\n", (unsigned long long)iterations);
printf(" \"repeats\": %d,\n", repeats);
printf(" \"shared_line_best_seconds\": %.4f,\n", packed_best);
printf(" \"shared_line_mean_seconds\": %.4f,\n", packed_sum / repeats);
printf(" \"padded_best_seconds\": %.4f,\n", padded_best);
printf(" \"padded_mean_seconds\": %.4f,\n", padded_sum / repeats);
printf(" \"shared_line_million_ops_per_sec\": %.2f,\n", total_ops / packed_best / 1e6);
printf(" \"padded_million_ops_per_sec\": %.2f,\n", total_ops / padded_best / 1e6);
printf(" \"slowdown_factor\": %.2f\n", packed_best / padded_best);
printf("}\n");
return 0;
}
Previous runs
| Date | Headline result |
|---|---|
| 2026-09-08 05:01 | 190.47 |
| 2026-09-08 04:56 | 208.63 |
| 2026-09-08 04:53 | 180.58 |