`__builtin_expect_with_probability(_, _, 0.99)` does not trigger select→branch on x86: `SelectOptimize` uses strict `>` against a 99/100 threshold
- Dominant language
- LLVM
- Stars
- 40.5k
- Forks
- 18.7k
- PR merge metrics
- PR metrics pending
Description
## Summary
`__builtin_expect_with_probability(c, val, p)` is documented as letting the caller specify the probability of the prediction. On x86 with default optimization, calling it with `p = 0.99` produces a `select` that is **not** expanded into a conditional branch by the `SelectOptimize` pass — even though the documented threshold for "highly predictable" is 99%, and even though plain `__builtin_expect(c, val)` (which corresponds to a less specific hint) *does* trigger the expansion at the same call site.
The result is that an honest user who measures branch predictability and supplies an accurate probability gets the slower lowering, while a user who omits the probability or rounds it up to 0.991 gets the faster one. On a tight loop the cost is significant (≥25% on Zen 2 in our measurements).
## Reproducer
Compiler Explorer (clang trunk, `-O3 -march=znver2 -fno-vectorize -fno-slp-vectorize`): https://godbolt.org/z/q4sMsGb17
The same kernel is compiled with four different probability values:
```cpp
#include
#include
extern "C" __attribute__((noinline))
std::int64_t run_50(int a, int b,
const std::uint8_t* __restrict cond,
std::int32_t* __restrict out, std::size_t n) {
std::int64_t acc = 0;
for (std::size_t i = 0; i < n; ++i) {
int v = __builtin_expect_with_probability(cond[i], 1, 0.50) ? a : b;
out[i] = v; acc += v;
}
return acc;
}
// ... run_99 with 0.99, run_991 with 0.991, run_999 with 0.999
```
Inner-loop lowering observed on clang trunk:
| Function | Probability | Inner loop | Lowering |
|---|---:|---|---|
| `run_50` | 0.50 | `cmpb` + `movl` + **`cmovel`** | cmov |
| `run_99` | 0.99 | same as `run_50` | cmov |
| `run_991` | 0.991 | `cmpb` + `je` + diamond | conditional branch |
| `run_999` | 0.999 | same as `run_991` | conditional branch |
For comparison, replacing `__builtin_expect_with_probability(cond[i], 1, p)` with plain `__builtin_expect(cond[i], 1)` produces the conditional-branch lowering, regardless of probability — because `__builtin_expect` corresponds to weights `{1, 2000}` (≈ 99.95%), well above the 99% threshold.
## Expected behavior
Calling `__builtin_expect_with_probability(c, v, 0.99)` should be at least as effective as calling `__builtin_expect(c, v)`. Both correspond to a "very likely" hint; documenting the probability should not silently weaken it.
## Actual behavior
`expect_with_probability(c, v, 0.99)` is treated by `SelectOptimize` as if no useful hint were present: the `select` stays as `cmov`. `__builtin_expect(c, v)` triggers the expansion to a branch. The two builtins produce the same IR shape (a `select` with `!prof !{!"branch_weights", !"expected", w_taken, w_not_taken}` metadata), but with different weight magnitudes:
| Source | Weights | Probability |
|---|---|---:|
| `__builtin_expect(c, 1)` | `{1, 2000}` | 99.95% |
| `expect_with_probability(c, 1, 0.99)` | `{21474838, 2126008811}` | 99.000% |
| `expect_with_probability(c, 1, 0.991)` | `{19327..., 2128...}` | 99.10% |
## Root cause
In `llvm/lib/CodeGen/SelectOptimize.cpp` (release/20.x):
```cpp
bool SelectOptimizeImpl::isSelectHighlyPredictable(const SelectLike SI) {
uint64_t TrueWeight, FalseWeight;
if (extractBranchWeights(SI, TrueWeight, FalseWeight)) {
uint64_t Max = std::max(TrueWeight, FalseWeight);
uint64_t Sum = TrueWeight + FalseWeight;
if (Sum != 0) {
auto Probability = BranchProbability::getBranchProbability(Max, Sum);
if (Probability > TTI->getPredictableBranchThreshold())
return true;
}
}
return false;
}
```
`TTI->getPredictableBranchThreshold()` is not overridden by the X86 backend (no override in `llvm/lib/Target/X86/X86TargetTransformInfo.{h,cpp}`), so the default in `llvm/include/llvm/Analysis/TargetTransformInfoImpl.h` applies:
```cpp
BranchProbability getPredictableBranchThreshold() const {
return BranchProbability(99, 100);
}
```
The comparison is strict `>`. A probability that is *equal* to 99/100 — exactly what `expect_with_probability(_, _, 0.99)` produces — is rejected. Plain `__builtin_expect` clears the bar by virtue of the `LikelyBranchWeight` default of 2000 in `LowerExpectIntrinsic.cpp`, which yields ≈99.95%, well above the threshold (the source comment at line 47 explicitly states the two defaults are coordinated).
`expect_with_probability` lets the user produce a probability that lands *exactly* on the threshold, where the strict `>` rejects it.
## Performance impact (illustrative)
On Zen 2 (Threadripper 3960X, isolated core, AVX vectorization disabled), `cond[i] ? a : b` lowered to cmov costs 0.54 ns/elem regardless of branch entropy. The same kernel as a conditional branch costs 0.40 ns/elem at ≥90% predictable data. So a user who annotates an accurate 90% probability via `expect_with_probability` gets ~35% slower codegen than one who uses `__builtin_expect`, `[[likely]]`, or `expect_with_probability(_, _, 0.991)`.
The actual cmov-vs-branch break-even on Zen 2 for this kernel sits at ~82% predictability. The 99% threshold is far above the real break-even, so any honest probability in the 0.82–0.99 range produces suboptimal codegen.
## Suggested fix(es)
Several plausible fixes, in increasing order of scope:
1. **Change `>` to `>=` in `SelectOptimize.cpp:1203`.** Smallest change. Fixes the 0.99-exactly corner. The broader threshold-too-high concern remains.
2. **Override `getPredictableBranchThreshold()` for X86** (and possibly other targets) with a value closer to the actual cmov-vs-branch break-even — empirically ~80–85% on Zen 2; likely similar on modern Intel. This matches the long-standing folklore that "branches above 80% predictability beat cmov" on TAGE-class predictors.
3. **Trust `expect_with_probability` weights as a stronger signal than the threshold.** When the user provided an explicit probability via `expect_with_probability`, treat it as a directive rather than a hint subject to TTI-level filtering. (Closer to the documented intent of the builtin.)
(1) is the obvious immediate fix. (2) is the systemically correct one but is more of a tuning change.
## Versions
- Reproduced on `clang version 20.1.2 (Ubuntu 24.04)`.
- Reproduced on **clang trunk** via Compiler Explorer (as of 2026-05-07): https://godbolt.org/z/q4sMsGb17 — both `run_50` and `run_99` emit `cmovel`; `run_991` and `run_999` emit a real conditional branch.
- Source inspected: LLVM `release/20.x` branch (paths and line numbers above are stable on `main` at the time of filing).
- Hardware: AMD Threadripper 3960X (Zen 2), `-march=znver2`. Codegen behavior verified across `-march={x86-64, x86-64-v3, x86-64-v4, haswell, skylake, icelake-server, sapphirerapids, alderlake, znver1–znver4}`.
Contributor guide
Assessment
This issue has not been assessed yet.