Skip to main content
UltraInstinct
← All benchmarks

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

Repeats5
Threads8
Slowdown Factor15.39
Padded Best Seconds0.0273
Padded Mean Seconds0.0291
Iterations Per Thread10000000
Shared Line Best Seconds0.42
Shared Line Mean Seconds0.5119
Padded Million Ops Per Sec2931.56
Shared Line Million Ops Per Sec190.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

CPUAMD EPYC 9354P 32-Core Processor
Cores visible8
Memory31.3 GB
Kernel6.8.0-139-generic
Architecturex64
Compilergcc (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

DateHeadline result
2026-09-08 05:01190.47
2026-09-08 04:56208.63
2026-09-08 04:53180.58