LLVM-EXEGESIS(1)

πŸ’– LLVM-EXEGESIS πŸ’–

okay bestie, sit down, hydrate, because we're about to talk about llvm-exegesis, straight out of the LLVM manual, section 1. she's a machine instruction benchmark and she's literally so powerful it's giving main character energy. no cap.


✨ NAME ✨

llvm-exegesis β€” LLVM Machine Instruction Benchmark πŸŽ€


✨ SYNOPSIS ✨

llvm-exegesis [options]

that's it. that's the whole vibe. just you, some options, and the raw truth about your CPU. πŸ’…


✨ DESCRIPTION ✨

so like, llvm-exegesis is a benchmarking tool that uses the info LLVM already knows about the world to measure host machine instruction characteristics β€” think latency, throughput, or port decomposition. she's nosy in the best way. πŸ¦‹

here's the tea: you give her an LLVM opcode name and a benchmarking mode, and she generates a code snippet that makes execution as serial (resp. as parallel) as possible β€” so that we can measure the latency (resp. inverse throughput / uop decomposition) of the instruction. the snippet gets jitted and executed on the host subtarget. the time taken (resp. the resource usage) is measured using hardware performance counters. and the result? printed out as YAML to standard output. slay. ✨

the main goal of this tool is to automatically (in)validate LLVM's TableDef scheduling models β€” like, is the model telling the truth or is it lying to us?? πŸ‘€ and to that end, we also provide analysis of the results, because receipts matter.

oh and pookie, llvm-exegesis can also benchmark arbitrary user-provided code snippets. she's flexible like that. 🫢


🌸 EXAMPLE 1: BENCHMARKING INSTRUCTIONS 🌸

okay so say you have an X86-64 machine (iconic). to measure the latency of a single instruction, run:

$ llvm-exegesis -mode=latency -opcode-name=ADD64rr

measuring the uop decomposition or inverse throughput of an instruction works basically the same way, no drama:

$ llvm-exegesis -mode=uops -opcode-name=ADD64rr
$ llvm-exegesis -mode=inverse_throughput -opcode-name=ADD64rr

the output is a YAML document β€” and the default is to write to stdout, but you can redirect the output to a file using -benchmarks-file. bestie options are everything. πŸ’…

---
key:
  opcode_name:     ADD64rr
  mode:            latency
  config:          ''
cpu_name:        haswell
llvm_triple:     x86_64-unknown-linux-gnu
num_repetitions: 10000
measurements:
  - { key: latency, value: 1.0058, debug_string: '' }
error:           ''
info:            'explicit self cycles, selecting one aliasing configuration.
Snippet:
ADD64rr R8, R8, R10
'
...

look at her. the structure. the little ... at the end. obsessed. 😭

now, to measure the latency of all instructions for the host architecture, run:

$ llvm-exegesis -mode=latency -opcode-index=-1

yes, -1. that's not a typo, that's a mood. πŸ”₯


πŸ¦‹ EXAMPLE 2: BENCHMARKING A CUSTOM CODE SNIPPET πŸ¦‹

to measure the latency/uops of a custom piece of code, you can specify the snippets-file option (- reads from standard input). cute.

$ echo "vzeroupper" | llvm-exegesis -mode=uops -snippets-file=-

real-life code snippets typically depend on registers or memory. so llvm-exegesis checks the liveliness of registers β€” meaning any register use has a corresponding def, or is a "live in". if your code depends on the value of some registers, you have two options:

  • mark the register as requiring a definition. llvm-exegesis will automatically assign a value to the register. this can be done using the directive LLVM-EXEGESIS-DEFREG <reg name> <hex_value>, where <hex_value> is a bit pattern used to fill <reg_name>. if <hex_value> is smaller than the register width, it will be sign-extended. ✨
  • mark the register as a "live in". llvm-exegesis will benchmark using whatever value was in this registers on entry. this can be done using the directive LLVM-EXEGESIS-LIVEIN <reg name>.

for example, the following code snippet depends on the values of XMM1 (which will be set by the tool) and the memory buffer passed in RDI (live in):

# LLVM-EXEGESIS-LIVEIN RDI
# LLVM-EXEGESIS-DEFREG XMM1 42
vmulps        (%rdi), %xmm1, %xmm2
vhaddps       %xmm2, %xmm2, %xmm3
addq $0x10, %rdi

look at those directives just sitting there being all declarative. we love a self-describing snippet. πŸŽ€


πŸ’œ EXAMPLE 3: ANALYSIS πŸ’œ

assuming you have a set of benchmarked instructions (either latency or uops) as YAML in file /tmp/benchmarks.yaml, you can analyze the results using the following command:

$ llvm-exegesis -mode=analysis \
  -benchmarks-file=/tmp/benchmarks.yaml \
  -analysis-clusters-output-file=/tmp/clusters.csv \
  -analysis-inconsistencies-output-file=/tmp/inconsistencies.html

this will group the instructions into clusters with the same performance characteristics. the clusters will be written out to /tmp/clusters.csv in the following format:

cluster_id,opcode_name,config,sched_class
...
2,ADD32ri8_DB,,WriteALU,1.00
2,ADD32ri_DB,,WriteALU,1.01
2,ADD32rr,,WriteALU,1.01
2,ADD32rr_DB,,WriteALU,1.00
2,ADD32rr_REV,,WriteALU,1.00
2,ADD64i32,,WriteALU,1.01
2,ADD64ri32,,WriteALU,1.01
2,MOVSX64rr32,,BSWAP32r_BSWAP64r_MOVSX64rr32,1.00
2,VPADDQYrr,,VPADDBYrr_VPADDDYrr_VPADDQYrr_VPADDWYrr_VPSUBBYrr_VPSUBDYrr_VPSUBQYrr_VPSUBWYrr,1.02
2,VPSUBQYrr,,VPADDBYrr_VPADDDYrr_VPADDQYrr_VPADDWYrr_VPSUBBYrr_VPSUBDYrr_VPSUBQYrr_VPSUBWYrr,1.01
2,ADD64ri8,,WriteALU,1.00
2,SETBr,,WriteSETCC,1.01
...

ngl those scheduling class strings are giving keyboard smash but they're doing their best and honestly? relatable. πŸ’€

llvm-exegesis will also analyze the clusters to point out inconsistencies in the scheduling information. the output is an html file. for example, /tmp/inconsistencies.html will contain messages like the following : [image]

⚠️ heads up bestie: note that the scheduling class names will be resolved only when llvm-exegesis is compiled in debug mode, else only the class id will be shown. this does not invalidate any of the analysis results though. so don't panic, the data is still the data. 🫢

okay that's the first chunk, and there's more coming so don't go anywhere β€” we're only getting started πŸ’–

✨ OPTIONS ✨

Okay bestie, this is the whole menu. Every single flag, no skips, we're eating the entire plate 🍽️✨

  • -help β€” prints a lil summary of all the command line options. It's giving "what are my choices" energy.
  • -opcode-index=<LLVM opcode index> β€” pick the opcode you wanna measure by index, bestie. And listen: if you set it to -1, it will measure every existing opcode (yes, all of them, absolute maximalist behavior πŸ’…). Peep example 1 for the deets. Also mandatory: you HAVE to set one of -opcode-index, -opcode-name, or -snippets-file. Not optional. Not negotiable. Periodt.
  • -opcode-name=<opcode name 1>,<opcode name 2>,... β€” same idea but you name the opcode instead of numbering it. And yes you can stack several of them as a comma-separated list, we love a bundle deal πŸŽ€. See example 1. And again: one of -opcode-index, -opcode-name, or -snippets-file must be set.
  • -snippets-file=<filename> β€” this is where you hand over your own custom code snippet to be measured. Check example 2. And yep, the trio rule again: -opcode-index, -opcode-name, or -snippets-file β€” at least one, always.
  • -mode=[latency|uops|inverse_throughput|analysis] β€” picks your run mode. Heads up though, some of these modes come with extra requirements and extra options, it's giving ✨high maintenance✨ but we respect it.

latency mode can make use of either RDTSC or LBR. The latency[LBR] flavor is only available on X86 (at least Skylake, so don't try it on your ancient laptop bestie 😭). And to actually run in latency mode, you need a positive value for x86-lbr-sample-period and -repetition-mode=loop.

In analysis mode you also need to specify at least one of -analysis-clusters-output-file= and -analysis-inconsistencies-output-file=.

  • -x86-lbr-sample-period=<nBranches/sample> β€” sets the LBR sampling period, aka how many branches go by before we grab a sample. If you give it a positive value and the mode is latency, we'll use LBRs to measure. Choosing the "right" sampling period: a small value is preferred, but if you sample too frequently you might get throttling, so like… chill a little 🌸. Also use a prime number so you don't consistently skip certain blocks. Math girlies stay winning.
  • -repetition-mode=[duplicate|loop|min] β€” picks how we repeat stuff:

duplicate builds one big straight-line basic block stuffed with num-repetitions instructions (it repeats the snippet num-repetitions/snippet size times).

loop will, optionally, duplicate the snippet until the loop body holds at least loop-body-size instructions, and then wraps the whole thing in a loop that executes num-repetitions instructions (so yeah, again it repeats the snippet num-repetitions/snippet size times). The loop mode, especially with loop unrolling, tends to hide the CPU frontend's effects better on architectures that cache decoded instructions β€” but it eats a register for counting iterations, so that's the tradeoff πŸ’€.

And if you're doing analysis over a whole bunch of opcodes, it may be best to use min mode instead, which runs every other mode and gives you the minimal measured result. Tiny but mighty.

  • -num-repetitions=<Number of repetitions> β€” sets the target number of executed instructions. Note that the actual repetition count of the snippet will be num-repetitions/snippet size. Higher values = more accurate measurements, but the benchmark gets longer. The eternal struggle fr fr.
  • -loop-body-size=<Preferred loop body size> β€” only does anything for -repetition-mode=[loop|min]. Instead of looping straight over the snippet, it first duplicates it so the loop body has at least this many instructions. Why? Because that can get the loop body cached in the CPU Op Cache / Loop Cache, which may have higher throughput than the CPU decoders. Cache girlies, we love you πŸ‘‘
  • -max-configs-per-opcode=<value> β€” caps how many configurations can be generated for each opcode. Default is 1, meaning we assume one single measurement is enough to characterize an opcode. But that's not true for every instruction: for example, the performance characteristics of the LEA instruction on X86 depend on the value of assigned registers and immediates πŸ‘€. Setting -max-configs-per-opcode larger than 1 lets llvm-exegesis explore more configurations to discover whether some register or immediate assignments lead to different performance characteristics. Detective mode activated πŸ”
  • -benchmarks-file=</path/to/file> β€” the file to read (analysis mode) or write (latency/uops/inverse_throughput modes) benchmark results. And "-" means use stdin/stdout. Flexible queen.
  • -analysis-clusters-output-file=</path/to/file> β€” if you provide it, the analysis clusters get written as CSV to this file. "-" prints to stdout. By default, this analysis is not run.
  • -analysis-inconsistencies-output-file=</path/to/file> β€” if it's non-empty, the inconsistencies found during analysis get written to this file. - prints to stdout. By default, this analysis is not run.
  • -analysis-clustering=[dbscan,naive] β€” picks the clustering algorithm. By default DBSCAN is used. The naive clustering algorithm is better for doing further work on the -analysis-inconsistencies-output-file= output β€” it creates one cluster per opcode and checks that the cluster is stable (all points are neighbours). Simple, honest, no drama.
  • -analysis-numpoints=<dbscan numPoints parameter> β€” sets the numPoints parameter used for DBSCAN clustering (analysis mode, DBSCAN only, so don't @ it otherwise).
  • -analysis-clustering-epsilon=<dbscan epsilon parameter> β€” sets the epsilon parameter used for clustering of benchmark points (analysis mode).
  • -analysis-inconsistency-epsilon=<epsilon> β€” sets the epsilon parameter used for detecting when the cluster differs from the LLVM schedule profile values (analysis mode).
  • -analysis-display-unstable-clusters β€” if there's more than one benchmark for an opcode, those benchmarks might not end up clustered into the same cluster when the measured performance characteristics are different. By default all such opcodes get filtered out. This flag instead shows you only the unstable opcodes. Unstable? No bestie, we're calling it eclectic πŸ’…
  • -ignore-invalid-sched-class=false β€” if set, ignore instructions that don't have a sched class (class idx = 0).
  • -mcpu=<cpu name> β€” if set, measure the cpu characteristics using the counters for this CPU. This is useful when you're creating new sched models and the host CPU is unknown to LLVM. The "who even are you" option 🫢
  • --dump-object-to-disk=true β€” by default, llvm-exegesis dumps the generated code to a temporary file so you can inspect it. You may disable it to speed up the execution and save disk space. Same energy as clearing your camera roll, honestly πŸ™ˆ

πŸ’€ EXIT STATUS πŸ’€

llvm-exegesis returns 0 on success. Otherwise, an error message is printed to standard error, and the tool returns a non 0 value. So if you see anything other than zero, something went wrong bestie, read the stderr.


πŸ‘‘ AUTHOR πŸ‘‘

Maintained by the LLVM Team (https://llvm.org/).


2003-2025, LLVM Project


And that's the whole page, bestie, we made it to the end πŸŽ€ This was LLVM-EXEGESIS(1), manual version 14, dated 2025-05-22 β€” go measure something iconic, you absolute main character. Slay, no cap, periodt. πŸ’–βœ¨πŸ¦‹

πŸ“¦ where this page comes from