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.