[llvm] [BOLT][AArch64] Relax calls and branches with fragment clusters (PR #215825)
Adam Bzowski via llvm-commits
llvm-commits at lists.llvm.org
Tue Sep 8 05:50:46 PDT 2026
adam-bzowski-arm wrote:
I had another look the current state of LongJmp + this patch.
[tests_2.zip](https://github.com/user-attachments/files/31957567/tests_2.zip)
These standalone reproducers demonstrate four issues in PR #215825 at `9aed40d7b6ec9a529ae97f6f1b8e43bdb8861b7a`.
## Issues
### Serious issues
1. Each relaxation page consists of up to 124 MiB of code followed by a 4 MiB thunk island. Consequently, branches between consecutive islands may span anywhere from 124 MiB to 132 MiB and are not guaranteed to fit in the Branch26 range.
2. BOLT classifies every inter-function `b` as a tail call and relaxes it through an `adrp/add/br` thunk. This sequence clobbers `x16`, which is valid for a real call but not necessarily for an arbitrary branch. Clang-generated C/C++ is not expected to contain such branches, but hand-written assembly, including assembly used by Chromium, does. The `x16`-clobbering sequence should be used only to relax `bl`; a general `b` needs a register-preserving solution.
### Unlikely issues
3. `adrp/add/br` does not provide unlimited range: `adrp` has a signed +/-4 GiB page-relative range. Chromium binaries in my builds are already 2.3-2.9 GiB, so this limit may become relevant.
4. A single basic block can itself exceed 128 MiB. If thunks can be inserted only at basic-block or fragment boundaries, a branch near the beginning of such a block cannot reach them.
## Reproducers
| Test | Issue | Observed failure |
|---|---|---|
| `consecutive-thunk-islands.s` | Consecutive thunk islands are not guaranteed to be mutually reachable. | Emission reports `fixup value out of range`. |
| `tail-classified-branch-live-x16.s` | Inter-function `b` to a skipped function clobbers live `x16`. | Original exits 0; rewritten output exits 1. |
| `tail-classified-body-branch-live-x16.s` | Inter-function `b` to a normally emitted function-body entry also clobbers live `x16`. | Original exits 0; rewritten output exits 1. |
| `skipped-target-4g-gap.s` | The `adrp` in a long thunk cannot reach its target. | JITLink reports an out-of-range `Page21` fixup. |
| `oversized-basic-block.s` | A thunk outside a basic block larger than 128 MiB is unreachable from its beginning. | Emission reports `fixup value out of range`. |
## Common setup
Run the commands from this directory and point `LLVM_BIN` at the LLVM build:
```sh
LLVM_BIN=/path/to/llvm-project/build/bin
```
## `consecutive-thunk-islands.s`
```sh
$LLVM_BIN/clang --target=aarch64-unknown-linux-gnu -fuse-ld=lld \
-nostdlib -Wl,-q -Wl,-e,A consecutive-thunk-islands.s \
-o consecutive-thunk-islands.exe
$LLVM_BIN/llvm-strip --strip-unneeded consecutive-thunk-islands.exe
$LLVM_BIN/llvm-bolt consecutive-thunk-islands.exe \
-o consecutive-thunk-islands.bolt \
--data consecutive-thunk-islands.fdata --split-functions \
--compact-code-model --relax-exp --max-cluster-size=134217728 \
--align-functions-max-bytes=0
```
Cluster 1 is modeled as 134,217,716 bytes (128 MiB minus 12). Eight emitted branch thunks make a thunk-to-thunk hop exactly 128 MiB, outside Branch26's representable range.
## `tail-classified-branch-live-x16.s`
This executable passes `42` in `x16` across a hand-written inter-function `b`. The destination exits with status 0 only if it receives that value. BOLT classifies the branch as a tail call and emits an `adrp/add/br` thunk using `x16`, so the rewritten executable exits with status 1.
```sh
$LLVM_BIN/clang --target=aarch64-unknown-linux-gnu -fuse-ld=lld \
-nostdlib -Wl,-q -Wl,-e,_start tail-classified-branch-live-x16.s \
-o tail-classified-branch-live-x16.exe
./tail-classified-branch-live-x16.exe
echo $? # 0
$LLVM_BIN/llvm-bolt tail-classified-branch-live-x16.exe \
-o tail-classified-branch-live-x16.bolt \
--relocs --lite=0 --relax-exp --skip-funcs='^destination$' \
--data tail-classified-branch-live-x16.fdata
./tail-classified-branch-live-x16.bolt
echo $? # 1
```
Under AAPCS64, `x16` is call-clobbered, so this is permitted for a genuine tail call. The reproducer demonstrates the risk of treating every inter-function `b` as a tail call when processing hand-written assembly.
## `tail-classified-body-branch-live-x16.s`
This variant does not skip any function. `destination` is emitted and reordered normally. `_start` branches to the global `STT_NOTYPE` symbol `destination_body` inside the function rather than to its entry. Profile ordering places a 129 MiB filler function between the source and destination.
```sh
$LLVM_BIN/clang --target=aarch64-unknown-linux-gnu -fuse-ld=lld \
-nostdlib -Wl,-q -Wl,-e,_start \
tail-classified-body-branch-live-x16.s \
-o tail-classified-body-branch-live-x16.exe
./tail-classified-body-branch-live-x16.exe
echo $? # 0
$LLVM_BIN/llvm-bolt tail-classified-body-branch-live-x16.exe \
-o tail-classified-body-branch-live-x16.bolt \
--relocs --lite=0 --relax-exp \
--data tail-classified-body-branch-live-x16.fdata \
--reorder-functions=exec-count
./tail-classified-body-branch-live-x16.bolt
echo $? # 1
```
BOLT builds three function-fragment clusters and creates an `__AArch64_forward_long_call_destination_body` thunk containing `adrp x16`/`add x16`/`br x16`. This confirms that normal emission and branching to a function-body entry do not avoid the tail-call classification or the resulting `x16` clobber.
## `skipped-target-4g-gap.s`
`skipped` remains in the original text while rewritten `_start` is emitted after a 4 GiB-plus `SHT_NOBITS` section. The executable therefore remains small. BOLT classifies the direct `b` as a tail call and creates a long `adrp/add/br` thunk, but the thunk cannot address `skipped`.
```sh
$LLVM_BIN/clang --target=aarch64-unknown-linux-gnu -fuse-ld=lld \
-nostdlib -Wl,-q -Wl,-e,_start skipped-target-4g-gap.s \
-o skipped-target-4g-gap.exe
$LLVM_BIN/llvm-bolt skipped-target-4g-gap.exe \
-o skipped-target-4g-gap.bolt --relocs --lite=0 --relax-exp \
--skip-funcs='^skipped$' --data skipped-target-4g-gap.fdata
```
Observed diagnostic:
```text
BOLT-ERROR: JITLink failed: ... relocation target ... (skipped) is out of range of Page21 fixup ...
```
## `oversized-basic-block.s`
This full-size stress case contains 33,554,432 real `add` instructions and uses neither `.space` nor BOLT padding.
```sh
$LLVM_BIN/clang --target=aarch64-unknown-linux-gnu -fuse-ld=lld \
-nostdlib -Wl,-q -Wl,-e,huge_bb oversized-basic-block.s \
-o oversized-basic-block.exe
$LLVM_BIN/llvm-bolt oversized-basic-block.exe \
-o oversized-basic-block.bolt --data oversized-basic-block.fdata \
--lite=0 --relax-exp --reorder-functions=exec-count
```
BOLT models the indivisible fragment as 134,217,768 bytes and creates one long thunk after it. The source call at the beginning cannot reach that thunk, so emission fails. In the recorded run, BOLT took 32 seconds and peaked at about 13.7 GiB RSS.
https://github.com/llvm/llvm-project/pull/215825
More information about the llvm-commits
mailing list