21 Sep 2026
Performance Analysis Approaches I
Performance Engineering Approaches
In a codebase:
- Take commit-by-commit benchmark measurements
- Profile and look at hotspots
- Use Marker APIs to binary search your code and optimise away poor performance.
3b. Tail performance can be studied and resolved using instrumentation
Glossary:
CPU/Hardware: cache misses, branch mispredictions, CPU-related events
OS/Software: Time, number of context switches, page faults
Linux perf can aggregate data from both
Another example is Tracy which combines Code Instrumentation, Tracing and Characterisation from multiple different sources.
Instrumentation
The most basic version of this is adding a print statement to a program
Best insights for big pieces of cdde, you can use a top-down approach to better understand the behaviour of an application. But you lose out on OS/CPU level insights.
Also as you can probably tell from adding print statements to leetcode solutions, instrumented code is often slower. This is because your instrumentation code usually ends up in the hot path which causes it to take more cycles.
Automatic Instrumentation: Done automatically to perform code coverage analysis or Profile-Guided Optimization.
See also binary instrumentation (static or dynamic). Its either static (ahead of time) or dynamic (insert binaries after compilation).
Intel Pin:
- instruction count, function calls
- instruction mix analysis
- intercepting function calls and execution of any instruction in an application (?)
- memory intensity and footprint
Overhead
All instrumentations have overhead. Most distributed systems can tolerate up to 5%. So if you need your measurements, you need to whittle the overhead down.
- Collecting info
- Storing info
- Reporting info
To reduce each
- Reduce collection
- Make use of online algorithms for mean, variance (e.g. Alon–Matias–Szegedy. Thanks SC2300)
Theres also Knuth's Online variance algo which apparently only takes 50 bytes of memory.
Tracing
blog for more: (strace Wow Much Syscall)
strace: instruments linux kernel
Intel Processor Traces: instruments CPU
Tracing is kind of like leveraging preinstalled instruments
strace runtime increases when the function you're tracing is system call heavy
Performance Monitor Counters
PMC multiplexing: when i have more events than physical counters, then i can reutilise the same counter to count for two different events, swapping between when i count for each event during distinct time samples. Then scale up. Issue occurs if trying to scale this up and hotspots exist.
Marker APIs
e.g. __itt_task_begin for Intel VTune, and amdProfileResume in AMG uProf
You can mark RFCs (remote function calls) for MarkerAPIs to preserve the granularity of instrumentation/tracing, but the CPU/OS level optimisations of PMCs.
read() system call will ensure atomic grouping, to prevent biases of certain metrics to certain phases, and to correlate groups of reads together.
Sampling
SIGPROF: A performance monitoring interrupt
Hotspots fall under here. Sometimes the concept here is poorly named as Sampling.
Sampling is basically...
User-mode sampling sets up threads and expiring timers than upon expiration the application receives a SIGPROF signal.
But EBS uses the PMC to trigger interrupts, and because it has the PMC then its allowed to do cool stuff like sample on L3 cache misses rather than cycles.
(its using the counter overflow feature, you ++ during the event and capture a sample when an overflow occurs and triggers an interrupt)
Use this to get a performance report
$ time -p perf record -F 1000 -- ./x264 -o /dev/null --slow --threads 1
../Bosphorus_1920x1080_120fps_420_8bit_YUV.y4m
[ perf record: Captured and wrote 1.625 MB perf.data (35035 samples) ]
real 36.20 sec
$ perf report -n --stdio
# Samples: 35K of event 'cpu_core/cycles/'
# Event count (approx.): 156756064947
Use this to go one level down and get a line-in-function report
# snippet of annotating source code of 'x264_8_me_search_ref' function
$ perf annotate x264_8_me_search_ref --stdio
Percent | Source code & Disassembly of x264 for cycles:ppp
----------------------------------------------------------
...
:
bmx += square1[bcost&15][0];
<== source code
1.43 : 4eb10d: movsx ecx,BYTE PTR [r8+rdx*2]
<== corresponding machine
code
:
bmy += square1[bcost&15][1];
0.36 : 4eb112: movsx r12d,BYTE PTR [r8+rdx*2+0x1]
:
bmx += square1[bcost&15][0];
0.63 : 4eb118: add
DWORD PTR [rsp+0x38],ecx
:
bmy += square1[bcost&15][1];
To unwind the Function Call Graph (knowing which function that called sqrt())
- Use frame pointer
- DWARF
- LBR (Last Branch Record, see next article)
uops Info Code Analyser — with a static analyser
(see UICA and llvm-mva as static tool analysers)
clang optimises the code into assembly like this. Then you get...vfmadd231ps. Thats...vector fuse multiply add operand2 * operand3 + operand1
so basically clang is trying to batch the sum operation, and leverage triple OOO to parallelise it
"Latest processors have 10+ execution units; how do I write my code to keep them busy all the time" this specifically can tell you how.
Use a Static Analyzer (Note: assumes L1 cache always hits, assumes no misprediction. Solves superscalarity problem)
Latency is E - D + 1
D is Dispatch, E is Executed
As a rule of thumb, in such situations, the loop must be unrolled by a factor of T L, where T is the throughput of an instruction, and L is its latency. In our case, we should have unrolled it by 2 4 = 8 to achieve maximum FMA port utilization since the throughput of FMA on Alder Lake is 2 and the latency of FMA is 4 cycles. This creates 8 separate data flow chains that can be executed independently.
Other Stuff
Roofline model
It's always either compute bound or bandwidth bound.
This model is more in the HPC world, less so in general purpose applications.
General rule: If high arithmetic intensity -> compute bound, vectorise with SIMD and pass work between threads.
If low arithmeitc intensity -> memory bound, improve memory access speed
Instrumentation Might Affect Performance
sometimes instrumentation is a core part of the code. E.g. if IPC dips too low, then defer low priority work.
$ clang-O3-Rpass-analysis=.*-Rpass=.*-Rpass-missed=.* a.c-c
a.c:5:3: remark: loop not vectorized [-Rpass-missed=loop-vectorize]
for (unsigned i = 1; i < N; i++) {
^
a.c:5:3: remark: unrolled loop by a factor of 8 with run-time trip count
[-Rpass=loop-unroll]
for (unsigned i = 1; i < N; i++) {
Compiler Hints
Compilers often decide whether a certain transformation will be beneficial based on their cost model analysis. But compilers don’t always make the optimal choice. Once you detect a key missing optimization in the report, you can attempt to rectify it by changing the source code or by providing hints to the compiler in the form of a #pragma, an attribute, a compiler built-in, etc. As always, verify your hypothesis by measuring it in a practical environment.