Optimise LLVM IR Safely with opt-20
You will run a real LLVM pass over an .ll module, write the transformed module to a separate file, and verify the result after each pass. This guide uses Ubuntu's llvm-20 package, version 1:20.1.8~++20250804090239+87f0227cb601-1~exp1~20250804210352.139, which provides LLVM 20.1.8. Allow about fifteen minutes if you have a small IR file ready.
The route
Jump straight to the step you need, or tick off Done means at the end.
- 1. Check the version and available passes
- Checkpoint: make a non-destructive test run
- 2. Optimise an LLVM assembly file into another file
- 3. Use a temporary output before replacing a useful file
- Checkpoint: understand the output format
- 4. Choose assembly or bitcode deliberately
- 5. Read failures without changing the input
You need a shell, a readable LLVM assembly or bitcode input, and enough space for a new output file. The examples run as an ordinary user. opt-20 does not need root unless your input or destination directory is itself restricted. Keep the original input: an optimiser output is a new build artefact, not an undoable edit of the source.
Checkpoint: confirm the installed tool.
1. Check the version and available passes
Start by confirming which executable you will run. The version matters because pass names and command-line interfaces depend on the LLVM release:
$ command -v opt-20
/usr/bin/opt-20
$ opt-20 --version
Ubuntu LLVM version 20.1.8
Optimized build.
Ask the installed binary for its pass list rather than copying a pass name from an unrelated LLVM release:
$ opt-20 -print-passes | head
Module passes:
always-inline
annotation2metadata
assign-guid
attributor
For LLVM 20, the modern pass manager accepts a pipeline through -passes=. The detailed manpage also documents the general pass-selection idea, but the executable's -help output is the final check for options available in this build.
Checkpoint: make a non-destructive test run
2. Optimise an LLVM assembly file into another file
Use -S when you want readable LLVM assembly and -o for the destination. This example runs the mem2reg pass, which is available in this installation and replaces a simple stack slot with SSA values:
$ opt-20 -S -passes='mem2reg' \
-o /path/to/module.optimised.ll \
/path/to/module.ll
The input is not overwritten. A successful run exits with status zero and creates the destination. Check both facts before using the result:
$ test -s /path/to/module.optimised.ll && echo 'output is non-empty'
output is non-empty
$ opt-20 -S -passes='verify' -o /tmp/module.checked.ll /path/to/module.optimised.ll
$ printf 'verify status: %s\n' "$?"
verify status: 0
The verification command writes another file so that it has an explicit output destination. It checks the module and emits no diagnostic on success. It does not prove that the optimisation matches your application requirements; it checks LLVM IR validity.
3. Use a temporary output before replacing a useful file
Shell redirection and -o can truncate an existing destination. If you are replacing a checked-in or otherwise valuable result, write beside it first and move it only after the command and verification succeed:
$ tmp=/path/to/module.optimised.ll.tmp
$ opt-20 -S -passes='mem2reg' -verify-each \
-o "$tmp" /path/to/module.ll
$ opt-20 -S -passes='verify' -o /tmp/module.final-check.ll "$tmp"
$ mv -- "$tmp" /path/to/module.optimised.ll
-verify-each adds verification after every pass selected on the command line. It is useful when a longer pipeline fails and you need to narrow down which pass produced invalid IR. The final mv changes the destination only after both commands return zero. If either command fails, inspect its diagnostic and remove the temporary file with rm -- /path/to/module.optimised.ll.tmp only when you are certain it is disposable.
Checkpoint: understand the output format
4. Choose assembly or bitcode deliberately
LLVM accepts assembly (.ll) and bitcode (.bc). Without -S, output is bitcode. With -S, output is textual LLVM assembly. This distinction affects both review and downstream tools:
$ opt-20 -passes='mem2reg' \
-o /path/to/module.optimised.bc \
/path/to/module.bc
$ opt-20 -S -passes='mem2reg' \
-o /path/to/module.optimised.ll \
/path/to/module.bc
Do not send raw bitcode to a terminal. The installed manpage documents -f for explicitly allowing binary output on a terminal, but that is rarely useful during normal work. Prefer -o to a file and use -S when you need to read the result.
5. Read failures without changing the input
A non-zero exit status means the run failed. Common causes are a misspelled or unavailable pass, malformed IR, an unreadable input, or an output path that cannot be created. Capture the status immediately and keep the diagnostic:
$ opt-20 -S -passes='mem2reg,verify' \
-o /path/to/module.optimised.ll /path/to/module.ll
$ status=$?
$ printf 'opt-20 status: %s\n' "$status"
$ test "$status" -eq 0
If a pass is rejected, run opt-20 -help and opt-20 -print-passes on the same machine. If the input is rejected, check the path and run the verifier on the unmodified module. Do not respond by adding sudo: elevated privileges do not repair invalid IR and can leave root-owned output files in your working tree.
Done means
opt-20 --versionidentifies the LLVM 20.1.8 executable you intended to use.- The selected pass appears in the installed pass list or help output.
- The original
.llor.bcfile remains unchanged. - The output format is intentional: readable assembly with
-S, or bitcode without it. - The output has passed
verify, and-verify-eachwas used when diagnosing a multi-pass pipeline.