Frontend Labs
⋯

42. Profiling: perf, cache-aware layout, and false sharing

42.1 Finding the actual bottleneck instead of guessing

perf record -g ./myapp
perf report                 # a sorted-by-time breakdown of where the CPU actually spent cycles
# macOS equivalent
sudo xctrace record --template 'Time Profiler' --launch -- ./myapp

Real-world example. "I think the bottleneck is the hash table" is a guess; perf report showing 40% of samples inside memcpy called from your logging function is evidence — profiling before optimizing is one of the highest-leverage habits for avoiding wasted performance work.

42.2 Struct-of-arrays vs. array-of-structs — Section 8.1's padding lesson, at scale

// Array-of-structs (AoS): natural, but wastes cache-line bandwidth if you
// only need one field across millions of elements
typedef struct { float x, y, z; int id; } ParticleAoS;
ParticleAoS particles[1000000];
// summing just .x touches every whole struct to use one field — wasted cache bandwidth

// Struct-of-arrays (SoA): every array is packed with only the field you need
typedef struct {
    float xs[1000000], ys[1000000], zs[1000000];
    int   ids[1000000];
} ParticlesSoA;
// summing xs[] walks the needed field contiguously — much better cache-line utilization

This is the kind of optimization physics engines, particle systems, and columnar databases use, because CPUs fetch memory in cache-line chunks (64 bytes is common on modern desktops/servers, but not guaranteed) — Section 8.1's struct-padding awareness, scaled up to "how should a million of these be laid out in memory."

42.3 False sharing — Section 20's atomics, sabotaged by layout

// BUG (performance, not correctness): two threads updating DIFFERENT
// counters that happen to share a CPU cache line will invalidate each
// other's cache line on every write, serializing what should be
// independent, parallel work.
typedef struct {
    atomic_int counter_a;   // likely on the same 64-byte cache line as counter_b
    atomic_int counter_b;
} Counters;

// Fix: pad so each counter owns its own cache line
typedef struct {
    atomic_int counter_a;
    char       pad[60];      // pushes counter_b onto the next 64-byte cache line
    atomic_int counter_b;
} CountersPadded;

This is a real, measured effect (not a micro-optimization myth) — the canonical demonstration is two threads each incrementing their own atomic_int in a tight loop, which runs dramatically slower when the two counters share a cache line than when padding separates them, purely from cache-coherency traffic between cores.


Generated from real-life-c-guide.md · 8 Oct 2026 16:36