LLVM-EXEGESIS(1)

🌸 NAME 🌸

llvm-exegesis β€” LLVM Machine Instruction Benchmark

Yeah bestie, this is the llvm-exegesis(1) page from the LLVM manual.

✨ SYNOPSIS ✨

llvm-exegesis [options]

πŸ’– DESCRIPTION πŸ’–

Okay so picture this, bestie: llvm-exegesis is a benchmarking tool that uses information available in LLVM to measure host machine instruction characteristics like latency, throughput, or port decomposition. Iconic.

You hand it an LLVM opcode name and a benchmarking mode, and it generates a code snippet that makes execution as serial (or as parallel, respectively) as possible so that we can measure the latency (or inverse throughput/uop decomposition, respectively) of the instruction. The code snippet is jitted and, unless requested not to, executed on the host subtarget. The time taken (or resource usage, respectively) is measured using hardware performance counters. The result is printed out as YAML to the standard output. Periodt.

The main goal of this tool is to automatically (in)validate LLVM's TableDef scheduling models. To that end, we also provide analysis of the results. Obsessed.

llvm-exegesis can also benchmark arbitrary user-provided code snippets. Fr fr.

πŸ¦‹ SUPPORTED PLATFORMS πŸ¦‹

Vibe check on platforms: llvm-exegesis currently only supports X86 (64-bit only), ARM (AArch64 only), MIPS, and PowerPC (PowerPC64LE only) on Linux for benchmarking. Not all benchmarking functionality is guaranteed to work on every platform. llvm-exegesis also has a separate analysis mode that is supported on every platform on which LLVM is.

πŸŽ€ SNIPPET ANNOTATIONS πŸŽ€

llvm-exegesis supports benchmarking arbitrary snippets of assembly. However, benchmarking these snippets often requires some setup so that they can execute properly. llvm-exegesis has two annotations and some additional utilities to help with setup so that snippets can be benchmarked properly. Here's the lineup:

  • LLVM-EXEGESIS-DEFREG <register name> β€” Adding this annotation to the text assembly snippet to be benchmarked marks the register as requiring a definition. A value will automatically be provided unless a second parameter, a hex value, is passed in. This is done with the LLVM-EXEGESIS-DEFREG <register name> <hex value> format. <hex value> is a bit pattern used to fill the register. If it is a value smaller than the register, it is sign extended to match the size of the register.
  • LLVM-EXEGESIS-LIVEIN <register name> β€” This annotation allows specifying registers that should keep their value upon starting the benchmark. Values can be passed through registers from the benchmarking setup in some cases. The registers and the values assigned to them that can be utilized in the benchmarking script with a LLVM-EXEGESIS-LIVEIN are as follows:
    • Scratch memory register β€” The specific register that this value is put in is platform dependent (e.g., it is the RDI register on X86 Linux). Setting this register as a live in ensures that a pointer to a block of memory (1MB) is placed within this register that can be used by the snippet.
  • LLVM-EXEGESIS-MEM-DEF <value name> <size> <value> β€” This annotation allows specifying memory definitions that can later be mapped into the execution process of a snippet with the LLVM-EXEGESIS-MEM-MAP annotation. Each value is named using the <value name> argument so that it can be referenced later within a map annotation. The size is specified in bytes, and the value is taken in hexadecimal. If the size of the value is less than the specified size, the value will be repeated until it fills the entire section of memory. Using this annotation requires using the subprocess execution mode.
  • LLVM-EXEGESIS-MEM-MAP <value name> <address> β€” This annotation allows for mapping previously defined memory definitions into the execution context of a process. The value name refers to a previously defined memory definition and the address is a decimal number that specifies the address the memory definition should start at. Note that a single memory definition can be mapped multiple times. Using this annotation requires the subprocess execution mode.

πŸ”₯ EXAMPLE 1: BENCHMARKING INSTRUCTIONS πŸ”₯

Assume you have an X86-64 machine. 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 similarly:

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

The output is a YAML document (the default is to write to stdout, but you can redirect the output to a file using -benchmarks-file):

---
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
'
...

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

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

🌈 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).

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

Real-life code snippets typically depend on registers or memory. llvm-exegesis checks the liveliness of registers (i.e. any register use has a corresponding def or is a β€œlive in”). If your code depends on the value of some registers, you need to use snippet annotations to ensure setup is performed properly.

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

πŸ“ EXAMPLE 3: BENCHMARKING WITH MEMORY ANNOTATIONS πŸ“

Some snippets require memory setup in specific places to execute without crashing. Setting up memory can be accomplished with the LLVM-EXEGESIS-MEM-DEF and LLVM-EXEGESIS-MEM-MAP annotations. To execute the following snippet:

movq $8192, %rax
movq (%rax), %rdi

We need to have at least eight bytes of memory allocated starting 0x2000. We can create the necessary execution environment with the following annotations added to the snippet:

# LLVM-EXEGESIS-MEM-DEF test1 4096 2147483647
# LLVM-EXEGESIS-MEM-MAP test1 8192

movq $8192, %rax
movq (%rax), %rdi

πŸ‘‘ EXAMPLE 4: 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
...

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]

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, bestie.

✨ OPTIONS ✨

Okay bestie, this is the part where the tool gets its whole personality. Every flag below is real, so don't go making up your own, mwah πŸ’‹

  • -help β€” Prints a summary of all the command line options. Your little "wait what was that flag again" saviour 🫢
  • -opcode-index=<LLVM opcode index> β€” Picks the opcode you wanna measure, but by index instead of by name. If you pass -1, it measures literally every existing opcode (iconic behaviour, honestly πŸ’…). Check example 1 for the deets. Heads up: you must set one of opcode-index, opcode-name, or snippets-file β€” no vibes-only runs allowed.
  • -opcode-name=<opcode name 1>,<opcode name 2>,... β€” Same thing but by name, and yes you can stack several of them in a comma-separated list. Example 1 has the tea. Again: one of opcode-index, opcode-name, or snippets-file has to be set, or nothing happens.
  • -snippets-file=<filename> β€” Points at your own custom code snippet to measure. Example 2 is your girl. And yeah β€” opcode-index, opcode-name, or snippets-file, at least one, always.
  • -mode=[latency|uops|inverse_throughput|analysis] β€” Sets the run mode. Quick warning: some modes come with extra requirements and options, so don't just yeet a mode in and expect magic.

latency mode can make use of either RDTSC or LBR. latency[LBR] is only available on X86 (at least Skylake). 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=. No output file, no analysis, that's the rule πŸ“‹

  • --benchmark-phase=[prepare-snippet|prepare-and-assemble-snippet|assemble-measured-code|measure] β€” So by default, when you set -mode=, the generated snippet gets executed and measured β€” which means you gotta be running on the actual hardware the snippet was generated for, and that hardware has to support performance measurements. But you can stop early at some stage before the measuring part. The choices, bestie:
    • prepare-snippet: Only generate the minimal instruction sequence.
    • prepare-and-assemble-snippet: Same as prepare-snippet, but also dumps an excerpt of the sequence (hex encoded).
    • assemble-measured-code: Same as prepare-and-assemble-snippet. but also creates the full sequence that can be dumped to a file using --dump-object-to-disk. (Yeah, there's a stray period in there, we don't fix, we respect ✨)
    • measure: Same as assemble-measured-code, but also runs the measurement.
  • -x86-lbr-sample-period=<nBranches/sample> β€” Sets the LBR sampling period β€” like, how many branches go by before we snag a sample. If you give this a positive value and the mode is latency, we're using LBRs for measuring. Picking the "right" sampling period: small values are preferred, but if sampling is too frequent you might get throttling. Also, use a prime number so you don't consistently skip certain blocks β€” sneaky little blocks 🧚
  • -x86-disable-upper-sse-registers β€” Using the upper xmm registers (xmm8-xmm15) forces a longer instruction encoding, and that can put more pressure on the frontend fetch and decode stages, which can potentially reduce the rate instructions get dispatched to the backend β€” especially on older hardware. Comparing baseline results against this mode enabled helps you figure out the frontend's effects, and it can be used to improve your latency and throughput estimates. Nerdy but make it fashion πŸ”₯
  • -repetition-mode=[duplicate|loop|min] β€” How we repeat the thing:
    • duplicate creates a large, straight line basic block with num-repetitions instructions (repeating the snippet num-repetitions/snippet size times).
    • loop will, optionally, duplicate the snippet until the loop body contains at least loop-body-size instructions, then wrap the whole result in a loop that executes num-repetitions instructions (so again: repeating the snippet num-repetitions/snippet size times). Loop mode, especially with loop unrolling, tends to hide the CPU frontend's effects better on architectures that cache decoded instructions β€” but it consumes a register for counting iterations, so, tradeoffs.
    • min is your girl if you're doing an analysis over many opcodes: it runs each other mode and produces the minimal measured result.
  • -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. Accuracy costs time, tragic πŸ’€
  • -loop-body-size=<Preferred loop body size> β€” Only effective for -repetition-mode=[loop|min]. Instead of looping over the snippet directly, it first duplicates it so the loop body contains at least this many instructions. Why? Because that potentially gets the loop body cached in the CPU Op Cache / Loop Cache, which may have higher throughput than the CPU decoders. Sneaky speed hack, lowkey slay.
  • -max-configs-per-opcode=<value> β€” Sets the max configurations that can be generated per opcode. Default is 1, meaning we assume one single measurement is enough to characterise an opcode. But that's not true of every instruction β€” for example, the performance characteristics of the LEA instruction on X86 depend on the value of assigned registers and immediates. Setting this larger than 1 lets llvm-exegesis explore more configurations to discover whether some register or immediate assignments lead to different performance characteristics. Curiosity is a virtue, periodt.
  • -benchmarks-file=</path/to/file> β€” The file to read (analysis mode) or write (latency/uops/inverse_throughput modes) benchmark results to. And β€œ-” uses stdin/stdout, cute πŸŽ€
  • -analysis-clusters-output-file=</path/to/file> β€” If provided, writes the analysis clusters as CSV to this file. β€œ-” prints to stdout. By default, this analysis is not run.
  • -analysis-inconsistencies-output-file=</path/to/file> β€” If non-empty, writes inconsistencies found during analysis to this file. - prints to stdout. By default, this analysis is not run.
  • -analysis-filter=[all|reg-only|mem-only] β€” By default, all benchmark results get analysed, but sometimes you only wanna look at the ones that don't involve memory, or the other way round. This option lets you either keep everything, or filter out (ignore) either all the ones that do involve memory (i.e. involve instructions that may read or write to memory), or the opposite β€” keep only those. Choose your fighter πŸ₯Š
  • -analysis-clustering=[dbscan,naive] β€” Which clustering algorithm to use. DBSCAN is the default. The naive one is better if you're 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).
  • -analysis-numpoints=<dbscan numPoints parameter> β€” Sets the numPoints parameter used for DBSCAN clustering (analysis mode, DBSCAN only).
  • -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 to detect 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 if the measured performance characteristics differ. By default, all such opcodes get filtered out. This flag instead shows only those unstable opcodes. Chaos mode, but make it informative πŸ™ˆ
  • -ignore-invalid-sched-class=false β€” If set, ignore instructions that don't have a sched class (class idx = 0).
  • -mtriple=<triple name> β€” The target triple. See -version for available targets.
  • -mcpu=<cpu name> β€” If set, measures the CPU characteristics using the counters for this CPU. Super useful when you're creating new sched models and the host CPU is unknown to LLVM. (-mcpu=help for details)
  • --analysis-override-benchmark-triple-and-cpu β€” By default, llvm-exegesis analyses the benchmarks for the triple/CPU they were measured for. But if you wanna analyse them for some other combination (specified via -mtriple/-mcpu), you pass this flag. Main character energy: do it your way πŸ‘‘
  • --dump-object-to-disk=true β€” If set, llvm-exegesis dumps the generated code to a temporary file so you can inspect it. Disabled by default.
  • --use-dummy-perf-counters β€” If set, llvm-exegesis won't read any real performance counters and returns a dummy value instead. Use it to make sure a snippet doesn't crash when hardware performance counters are unavailable, and for debugging llvm-exegesis itself. Fake it till you make it πŸ’…
  • --execution-mode=[inprocess,subprocess] β€” Picks the execution mode. inprocess is the default. subprocess unlocks extra features like memory annotations, but is currently restricted to X86-64 on Linux. Sad but true, bestie 😭

🚦 EXIT STATUS 🚦

llvm-exegesis returns 0 on success. Otherwise, an error message gets printed to standard error and the tool returns a non 0 value. So like, if it's not 0, go read the stderr, she's trying to tell you something 🫢

πŸ‘‘ Author πŸ‘‘

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

2003-2026, LLVM Project


And that's the whole page, bestie β€” no cap, you now know LLVM-EXEGESIS(1) better than most people know their own passwords 🌈 This beauty's from the LLVM Project, version/date stamp reads 15 and 2026-09-09, so go forth and benchmark responsibly. Mwah! πŸ’–πŸ¦‹βœ¨

πŸ“¦ where this page comes from