cpu-cache-opt
Use when diagnosing cache misses with perf, fixing false sharing, choosing AoS or SoA layout, or adding software prefetch. Not for cache theory: use memory-hierarchy-and-caches.
Install
npx skills add https://github.com/OutlineDriven/outline-driven-development/tree/main/.devin/skills/cpu-cache-opt
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install outlinedriven-outline-driven-development@llmmart
git clone https://github.com/OutlineDriven/outline-driven-development.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole outlinedriven/outline-driven-development collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
CPU cache optimization
Cache performance is a layout property before it is a code property. Measure with hardware counters, move the data so the working set fits, and re-measure. Every change must show up in the counters, or it reverts.
Contract
| Field | Bound contract |
|---|---|
| Trigger | The task diagnoses cache misses, detects or fixes false sharing, restructures data layout, evaluates AoS versus SoA, or decides on software prefetch. |
| Authority | Read-only. The skill runs performance tools that read the program and prints guidance; source and build changes land through the normal coding path. No remote mutation. |
| Side effect | None beyond tool output files that the profiling tools write to their own scratch locations. |
| Done | A measured before and after for the proposed layout change exists, with the cache counters quoted, or the diagnosis names the miss class and the evidence for it. |
Inputs
- The program and a reproducible workload: required. A benchmark that runs the hot loop long enough to fill counters.
- The build: required, with symbols for attribution and without changing optimization between measurements.
- The target machine: required. Counters and latencies are properties of the specific microarchitecture.
Procedure
- Measure before changing anything. Take the generic counters first, then the level-specific ones. Done when: a baseline of counts and miss rates for the real workload is recorded.
perf stat -e cache-references,cache-misses,cycles,instructions ./bench
perf stat -e L1-dcache-loads,L1-dcache-load-misses,LLC-loads,LLC-load-misses ./bench
Interpret relatively. An L1 miss rate that is acceptable for a pointer-chasing graph walk is severe for a streaming numeric kernel; judge each rate against the same program on the same machine, before against after, and against the memory-bound share of the run.
- Confirm false sharing before padding it. Threads writing to distinct variables that share one line invalidate each other constantly. The Intel HITM events count the modified-line hits that mark it. Done when: the counter either shows the traffic or rules the hypothesis out.
perf stat -e mem_load_l3_hit_retired.xsnp_hitm,machine_clears.memory_ordering ./bench
Event availability differs across vendors and generations; check perf list on the target machine and treat a missing event as unknown, not as zero.
- Apply the layout fixes in order of cost, cheapest first. Done when: each applied fix has a re-measurement from step 1.
- Split hot from cold fields so the hot struct stays one line:
struct record_hot { int id; int value; }; // touched every iteration
struct record_cold { char name[128]; char desc[256]; };
- Pad or align per-thread data to its own line:
struct alignas(64) padded_counter {
int value;
// the padding keeps the next counter on another line
};
In C++17 prefer std::hardware_destructive_interference_size over the literal 64, and read the real line size at runtime with sysconf(_SC_LEVEL1_DCACHE_LINESIZE). Sixty-four bytes is the common line on x86-64 and ARM server cores; some consumer and Apple cores use 128.
- Convert array-of-structs to struct-of-arrays when the loop touches few fields. Reading
x[i]from an SoA stream loads onlyx, and the access vectorizes; the same loop over an AoS drags every field through every line.
Remove the pointer chasing that no prefetcher predicts. Linked structures miss once per node. Pool-allocate nodes, replace links with indices into an array, or sort the traversal order to match memory order. Done when: the hot loop's accesses are sequential or the chasing is provably off the critical path.
Add software prefetch only where the pattern defeats the hardware prefetcher, such as linked lists and irregular graphs. The hint is a hint; measure or remove it. Done when: a counter or timing improvement proves the hint earns its place.
// issue the hint far enough ahead to cover latency,
// and early enough that the line is not evicted first
for (Node *n = head; n; n = n->next) {
if (n->next) __builtin_prefetch(n->next, 0, 1);
process(n);
}
_mm_prefetch on x86 takes a locality hint (_MM_HINT_T0 for L1, T1 for L2, T2 for L3, NTA for non-temporal). Prefetch distance is a tunable per machine, not a constant.
- Block the loop when the working set exceeds the cache. Choose the block size so one block of the working arrays fits the data cache, and tune the size on the target machine. Done when: the blocked version beats the naive one in step 1's counters.
// process cache-sized tiles instead of whole rows
#define BLOCK 64 // tune on the target machine
for (int i = 0; i < N; i += BLOCK)
for (int k = 0; k < N; k += BLOCK)
for (int j = 0; j < N; j += BLOCK)
for (int ii = i; ii < i + BLOCK && ii < N; ii++)
for (int kk = k; kk < k + BLOCK && kk < N; kk++)
for (int jj = j; jj < j + BLOCK && jj < N; jj++)
C[ii*N+jj] += A[ii*N+kk] * B[kk*N+jj];
- Verify allocation alignment when the transformation depends on it.
aligned_alloc(64, size)andposix_memaligngive line-aligned buffers; check struct layout withpahole -C MyStruct ./prog, and let-Wpaddedreport compiler-side padding. Done when: the layout the code assumes is the layoutpaholeprints.
Failure and recovery
| Failure class | Behavior |
|---|---|
| Counters read zero | The event is unavailable or virtualization hides it. Run perf list on the target and pick supported events. |
| No change after a layout fix | The loop was not miss-bound. Re-profile for the real bottleneck before the next transform. |
| SoA made it slower | The loop needs whole records, so SoA splits them across lines. Keep AoS and split only the hot fields. |
| Prefetch hurt | The hint came too early and evicted useful lines, or the hardware prefetcher already covered it. Remove the hint. |
| Two threads still slow | The sharing moved. Re-run the HITM counters from step 2 on the new layout. |
Output
A measurement report: baseline counters, each transformation applied, and the counters after it, with the winning layout named. Counter names, Cachegrind usage, and layout tooling are in references/cache-counters.md.
Files (outline-driven-development)
-
agents
-
openai.yaml 189 B
interface: display_name: "Cpu Cache Opt" short_description: "Use when diagnosing cache misses with perf, fixing false sharing, choosing AoS or SoA layout, or adding software prefetch."
-
-
references
-
cache-counters.md 3 KB
# Cache performance counters reference ## perf stat cache events Generic, portable across CPUs: | Event | Meaning | |-------|---------| | `cache-references` | Last-level cache accesses | | `cache-misses` | Last-level cache misses | x86 Intel PMU events for a level breakdown: ```bash perf stat -e \ L1-dcache-loads,L1-dcache-load-misses, \ L2-dcache-loads,L2-dcache-load-misses, \ LLC-loads,LLC-load-misses ./prog ``` Retired-load events on Intel: ```bash perf stat -e \ mem_load_retired.l1_miss, \ mem_load_retired.l2_miss, \ mem_load_retired.l3_miss, \ mem_inst_retired.all_loads ./prog ``` Event names differ by vendor and generation. Run `perf list` on the target machine and confirm each name before scripting it; a name the PMU lacks reports zero or errors, never a warning. ## False sharing detection HITM events count cache lines that were modified elsewhere and hit in a shared state, which is the signature of two cores fighting over one line: ```bash perf stat -e \ mem_load_l3_hit_retired.xsnp_hitm, \ mem_load_l3_miss_retired.remote_hitm, \ machine_clears.memory_ordering ./prog ``` ## Interpreting rates Compute miss rates from the counters of one run and compare them against the same program, same machine, different layout. There is no universal healthy threshold: a pointer-chasing workload tolerates rates that would sink a streaming kernel, and hardware prefetchers change the meaning of a raw count. Report rates as before and after pairs. ## Cachegrind, the simulator ```bash valgrind --tool=cachegrind --cache-sim=yes \ --I1=32768,8,64 --D1=32768,8,64 --LL=6291456,12,64 ./prog cg_annotate cachegrind.out.* --auto=yes cg_diff cachegrind.out.before cachegrind.out.after ``` Cachegrind models a fixed cache geometry, so use it for relative comparisons between layouts, not for absolute rates. ## Struct layout analysis ```bash pahole -C MyStruct ./myapp gcc -g -O0 -Wpadded -c hot.c ``` `pahole` output: ```text struct MyStruct { int x; /* 0 4 */ /* XXX 4 bytes hole, try to pack */ double y; /* 8 8 */ int z; /* 16 4 */ /* size: 24, cachelines: 1 */ }; ``` ## Aligned allocation ```c #include <stdlib.h> void *buf = aligned_alloc(64, 1024 * sizeof(float)); // C11 void *buf2; posix_memalign(&buf2, 64, 1024 * sizeof(float)); // POSIX ``` ```cpp alignas(64) float buf[1024]; // stack ``` ## Hardware prefetcher behavior The prefetcher detects sequential streams and constant strides. It cannot follow pointer chasing or computed indices. Manual prefetch earns its keep where the address is known early but the pattern is irregular, as in linked lists and sparse graphs: ```c Node *n = head; while (n) { if (n->next) __builtin_prefetch(n->next, 0, 1); process(n); n = n->next; } ``` Prefetch distance is a per-machine tunable: too early and the line is evicted before use, too late and the latency is not hidden. Measure with the counters above and keep only what wins.
-
-
SKILL.md 6.4 KB
--- name: cpu-cache-opt description: 'Use when diagnosing cache misses with perf, fixing false sharing, choosing AoS or SoA layout, or adding software prefetch. Not for cache theory: use memory-hierarchy-and-caches.' --- # CPU cache optimization Cache performance is a layout property before it is a code property. Measure with hardware counters, move the data so the working set fits, and re-measure. Every change must show up in the counters, or it reverts. ## Contract | Field | Bound contract | |---|---| | Trigger | The task diagnoses cache misses, detects or fixes false sharing, restructures data layout, evaluates AoS versus SoA, or decides on software prefetch. | | Authority | Read-only. The skill runs performance tools that read the program and prints guidance; source and build changes land through the normal coding path. No remote mutation. | | Side effect | None beyond tool output files that the profiling tools write to their own scratch locations. | | Done | A measured before and after for the proposed layout change exists, with the cache counters quoted, or the diagnosis names the miss class and the evidence for it. | ## Inputs - The program and a reproducible workload: required. A benchmark that runs the hot loop long enough to fill counters. - The build: required, with symbols for attribution and without changing optimization between measurements. - The target machine: required. Counters and latencies are properties of the specific microarchitecture. ## Procedure 1. Measure before changing anything. Take the generic counters first, then the level-specific ones. Done when: a baseline of counts and miss rates for the real workload is recorded. ```bash perf stat -e cache-references,cache-misses,cycles,instructions ./bench perf stat -e L1-dcache-loads,L1-dcache-load-misses,LLC-loads,LLC-load-misses ./bench ``` Interpret relatively. An L1 miss rate that is acceptable for a pointer-chasing graph walk is severe for a streaming numeric kernel; judge each rate against the same program on the same machine, before against after, and against the memory-bound share of the run. 2. Confirm false sharing before padding it. Threads writing to distinct variables that share one line invalidate each other constantly. The Intel HITM events count the modified-line hits that mark it. Done when: the counter either shows the traffic or rules the hypothesis out. ```bash perf stat -e mem_load_l3_hit_retired.xsnp_hitm,machine_clears.memory_ordering ./bench ``` Event availability differs across vendors and generations; check `perf list` on the target machine and treat a missing event as unknown, not as zero. 3. Apply the layout fixes in order of cost, cheapest first. Done when: each applied fix has a re-measurement from step 1. - Split hot from cold fields so the hot struct stays one line: ```c struct record_hot { int id; int value; }; // touched every iteration struct record_cold { char name[128]; char desc[256]; }; ``` - Pad or align per-thread data to its own line: ```c struct alignas(64) padded_counter { int value; // the padding keeps the next counter on another line }; ``` In C++17 prefer `std::hardware_destructive_interference_size` over the literal 64, and read the real line size at runtime with `sysconf(_SC_LEVEL1_DCACHE_LINESIZE)`. Sixty-four bytes is the common line on x86-64 and ARM server cores; some consumer and Apple cores use 128. - Convert array-of-structs to struct-of-arrays when the loop touches few fields. Reading `x[i]` from an SoA stream loads only `x`, and the access vectorizes; the same loop over an AoS drags every field through every line. 4. Remove the pointer chasing that no prefetcher predicts. Linked structures miss once per node. Pool-allocate nodes, replace links with indices into an array, or sort the traversal order to match memory order. Done when: the hot loop's accesses are sequential or the chasing is provably off the critical path. 5. Add software prefetch only where the pattern defeats the hardware prefetcher, such as linked lists and irregular graphs. The hint is a hint; measure or remove it. Done when: a counter or timing improvement proves the hint earns its place. ```c // issue the hint far enough ahead to cover latency, // and early enough that the line is not evicted first for (Node *n = head; n; n = n->next) { if (n->next) __builtin_prefetch(n->next, 0, 1); process(n); } ``` `_mm_prefetch` on x86 takes a locality hint (`_MM_HINT_T0` for L1, `T1` for L2, `T2` for L3, `NTA` for non-temporal). Prefetch distance is a tunable per machine, not a constant. 6. Block the loop when the working set exceeds the cache. Choose the block size so one block of the working arrays fits the data cache, and tune the size on the target machine. Done when: the blocked version beats the naive one in step 1's counters. ```c // process cache-sized tiles instead of whole rows #define BLOCK 64 // tune on the target machine for (int i = 0; i < N; i += BLOCK) for (int k = 0; k < N; k += BLOCK) for (int j = 0; j < N; j += BLOCK) for (int ii = i; ii < i + BLOCK && ii < N; ii++) for (int kk = k; kk < k + BLOCK && kk < N; kk++) for (int jj = j; jj < j + BLOCK && jj < N; jj++) C[ii*N+jj] += A[ii*N+kk] * B[kk*N+jj]; ``` 7. Verify allocation alignment when the transformation depends on it. `aligned_alloc(64, size)` and `posix_memalign` give line-aligned buffers; check struct layout with `pahole -C MyStruct ./prog`, and let `-Wpadded` report compiler-side padding. Done when: the layout the code assumes is the layout `pahole` prints. ## Failure and recovery | Failure class | Behavior | |---|---| | Counters read zero | The event is unavailable or virtualization hides it. Run `perf list` on the target and pick supported events. | | No change after a layout fix | The loop was not miss-bound. Re-profile for the real bottleneck before the next transform. | | SoA made it slower | The loop needs whole records, so SoA splits them across lines. Keep AoS and split only the hot fields. | | Prefetch hurt | The hint came too early and evicted useful lines, or the hardware prefetcher already covered it. Remove the hint. | | Two threads still slow | The sharing moved. Re-run the HITM counters from step 2 on the new layout. | ## Output A measurement report: baseline counters, each transformation applied, and the counters after it, with the winning layout named. Counter names, Cachegrind usage, and layout tooling are in `references/cache-counters.md`.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.