[ProfInfo][BFI] BFI analysis incorrectly computing positive counts for unreachable code
- Dominant language
- LLVM
- Stars
- 40.5k
- Forks
- 18.7k
- PR merge metrics
- PR metrics pending
Description
### BFI analysis computes positive counts for unreachable code causing it to be considered hot
In the IR module below, we observed positive block frequencies for unreachable basic blocks immediately after attaching a profile. We found that weights assigned to non‑profiled blocks during BFI analysis are scaled up when subsequent nested loops are analyzed. Ultimately, this causes blocks in the innermost loop to be assigned a non‑negligible frequency, leading to a positive BFI count. Notably, most of the code involved in these computations is completely unreachable.
To show how this behavior does not occur only on unreachable code, we also provide a code variant where the same hotness distortion happens on code reachable but not executed.
We tested this behavior using LLVM commit [`2563006f3178a6443cd2f05b9e4383ce2a4e7003`](https://github.com/llvm/llvm-project/commit/2563006f3178a6443cd2f05b9e4383ce2a4e7003).
We provide an archive with the IR module and profiles for reproduction [files.zip](https://github.com/user-attachments/files/26595600/files.zip)
```llvm
; ModuleID = '/root/module.ll'
source_filename = "/root/module.ll"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
target triple = "x86_64-unknown-linux-gnu"
@b = global i32 3
define void @a() {
bb:
ret void
}
; Function Attrs: noinline
define i16 @func_15() #0 {
bb:
br label %bb1
bb1: ; preds = %bb
br label %bb2
bb2: ; preds = %bb1
%i = load i32, ptr @b, align 4
%i3 = icmp ne i32 %i, 0
br i1 %i3, label %bb4, label %bb5
bb4: ; preds = %bb2
br label %bb18
bb5: ; preds = %bb2
br i1 true, label %bb6, label %bb17
bb6: ; preds = %bb5
store i16 0, ptr null, align 2
br label %bb7
bb7: ; preds = %bb8, %bb6
br i1 false, label %bb8, label %bb16
bb8: ; preds = %bb9, %bb7
br i1 false, label %bb9, label %bb7
bb9: ; preds = %bb10, %bb8
br i1 false, label %bb10, label %bb8
bb10: ; preds = %bb11, %bb9
br i1 false, label %bb11, label %bb9
bb11: ; preds = %bb13, %bb10
br i1 false, label %bb12, label %bb10
bb12: ; preds = %bb11
call void @a()
br label %bb13
bb13: ; preds = %bb15, %bb12
br i1 false, label %bb14, label %bb11
bb14: ; preds = %bb13
br label %bb15
bb15: ; preds = %bb14
br label %bb13
bb16: ; preds = %bb7
ret i16 0
bb17: ; preds = %bb5
ret i16 0
bb18: ; preds = %bb4
br label %bb19
bb19: ; preds = %bb18
ret i16 0
}
define i16 @main() {
bb:
%i = call i16 @func_15()
ret i16 %i
}
attributes #0 = { noinline }
```
#### Reproduction
We provide here the steps to replicate this finding starting from the given IR and profile. The following commands will attach the profile to the IR and dump the BFI.
First, we attach the profile using `opt` with `pgo-instr-use`. To show *only* the BFI analysis issue, we disable the function entry count adjustment performed by [`fixFuncEntryCount`](https://github.com/llvm/llvm-project/blob/main/llvm/lib/Transforms/Instrumentation/PGOInstrumentation.cpp#L2030). Later, we will discuss what happens with this step enabled (default behavior).
```bash
opt module.ll -passes='pgo-instr-use' -pgo-fix-entry-func=false -pgo-test-profile-file=module.profdata -S -o profiled.ll
```
```bash
opt -passes='print' -disable-output profiled.ll
```
```
Printing analysis results of BFI for function 'func_15':
block-frequency-info: func_15
- bb: float = 1.0, int = 18014398509481984, count = 50
- bb1: float = 1.0, int = 18014398509481984, count = 50
- bb2: float = 1.0, int = 18014398509481984, count = 50
- bb4: float = 1.0, int = 18014398501093376, count = 50
- bb5: float = 0.00000000046566, int = 8388608, count = 0
- bb6: float = 0.00000000023283, int = 4194304, count = 0
- bb7: float = 0.0000000074506, int = 134217728, count = 0
- bb7.bb8_crit_edge: float = 0.0000000072177, int = 130023424, count = 0
- bb8: float = 0.00000023097, int = 4160749568, count = 0
- bb8.bb9_crit_edge: float = 0.00000022375, int = 4030726144, count = 0
- bb9: float = 0.00000716, int = 128983236638, count = 0
- bb9.bb10_crit_edge: float = 0.0000069363, int = 124952510493, count = 0
- bb10: float = 0.00022196, int = 3998480335778, count = 0
- bb10.bb11_crit_edge: float = 0.00021502, int = 3873527825285, count = 0
- bb11: float = 0.00063163, int = 11378380626832, count = 0
- bb12: float = 0.0004166, int = 7504852801546, count = 0
- bb13: float = 0.013331, int = 240155289649484, count = 1
- bb14: float = 0.012915, int = 232650436847938, count = 1
- bb15: float = 0.012915, int = 232650436847938, count = 1
- bb16: float = 0.00000000023283, int = 4194304, count = 0
- bb17: float = 0.00000000023283, int = 4194304, count = 0
- bb18: float = 1.0, int = 18014398501093376, count = 50
- bb19: float = 1.0, int = 18014398501093376, count = 50
```
The BFI frequency for basic blocks `bb13`, `bb14`, and `bb15` is high enough to produce a non‑zero count.
#### Detailed analysis
This happens because BFI analysis assigns a [positive weight (1)](https://github.com/llvm/llvm-project/blob/main/llvm/lib/Analysis/BlockFrequencyInfoImpl.cpp#L317) to blocks that are not profiled (in this case `bb5`) to avoid division by zero. This value is initially negligible and does not affect most block frequencies. However, it is then amplified by scaling applied for the subsequent nested loops, causing basic blocks in the deepest nested loop to have a non‑zero frequency (1.3-1.6%, count = 1) although unreachable.
All loop scaling factors (computed from BPI) are completely off if the code logic is taken into account. In fact, all nested loops would have zero iterations (if reachable) because of the constant condition in their header blocks.
Because of BPI’s [loop heuristics](https://github.com/llvm/llvm-project/blob/main/llvm/lib/Analysis/BranchProbabilityInfo.cpp#L76), the probabilities are set to 97% (124/128) on the true branch and 3% (4/128) on the false branch even if the condition is `br i1 false`. This yields a loop scale of `32.0`, i.e., `1 / loop exit probability -> 1 / (4 / 128)`.
Applying this scaling once per nested loop exponentially increases the block frequency, ultimately leading to the positive counts. This scaling is visible in the BFI output above, where the frequency increases from `bb5` through `bb15`.
To better understand the BPI results, we include the control-flow graph of `func_15` with branch probabilities attached on edges.
We can clearly see in dark red the profiled path. The most interesting part is the alternative path below.
Starting from `bb5` we observe the following:
- `bb5` (`br i1 true`): the condition is `true` but the probabilities are 50/50.
- `bb7` (`br i1 false`): the condition is `false` but the probabilities are always leaning towards the `true` edge.
- `bb8`, `bb10`, `bb11` and `bb12` (`br i1 false`): the condition is `false` but the probabilities are always leaning towards the `true` edge (note that this code is unreachable).
These incorrect probabilities cause the upscaling of BFI weights for unreachable code and are responsible for the issue shown above.
#### Effects on `fixFuncEntryCount`
When `-pgo-fix-entry-count` is enabled, non‑zero frequencies on unreachable code cause the entry count of `func_15` to be rescaled (from 50 to 45) and this change is propagated to the rest of the blocks.
```bash
opt module.ll -passes='pgo-instr-use' -pgo-test-profile-file=module.profdata -S -o profiled-w-fix.ll
```
```bash
opt -passes='print' -disable-output profiled-w-fix.ll
```
```
Printing analysis results of BFI for function 'func_15':
block-frequency-info: func_15
- bb: float = 1.0, int = 18014398509481984, count = 45
- bb1: float = 1.0, int = 18014398509481984, count = 45
- bb2: float = 1.0, int = 18014398509481984, count = 45
- bb4: float = 1.0, int = 18014398501093376, count = 45
- bb5: float = 0.00000000046566, int = 8388608, count = 0
- bb6: float = 0.00000000023283, int = 4194304, count = 0
- bb7: float = 0.0000000074506, int = 134217728, count = 0
- bb7.bb8_crit_edge: float = 0.0000000072177, int = 130023424, count = 0
- bb8: float = 0.00000023097, int = 4160749568, count = 0
- bb8.bb9_crit_edge: float = 0.00000022375, int = 4030726144, count = 0
- bb9: float = 0.00000716, int = 128983236638, count = 0
- bb9.bb10_crit_edge: float = 0.0000069363, int = 124952510493, count = 0
- bb10: float = 0.00022196, int = 3998480335778, count = 0
- bb10.bb11_crit_edge: float = 0.00021502, int = 3873527825285, count = 0
- bb11: float = 0.00063163, int = 11378380626832, count = 0
- bb12: float = 0.0004166, int = 7504852801546, count = 0
- bb13: float = 0.013331, int = 240155289649484, count = 1
- bb14: float = 0.012915, int = 232650436847938, count = 1
- bb15: float = 0.012915, int = 232650436847938, count = 1
- bb16: float = 0.00000000023283, int = 4194304, count = 0
- bb17: float = 0.00000000023283, int = 4194304, count = 0
- bb18: float = 1.0, int = 18014398501093376, count = 45
- bb19: float = 1.0, int = 18014398501093376, count = 45
```
To understand this behavior, we added debug prints in `fixFuncEntryCount` to log the differences between real counts and BFI counts, and the scale factor computed as the ratio of their sums.
```
BB bb: Count=50, BFI Count=50
BB bb1: Count=50, BFI Count=50
BB bb2: Count=50, BFI Count=50
BB bb4: Count=50, BFI Count=50
BB bb5: Count=0, BFI Count=0
BB bb6: Count=0, BFI Count=0
BB bb7: Count=0, BFI Count=0
BB bb7.bb8_crit_edge: Count=0, BFI Count=0
BB bb8: Count=0, BFI Count=0
BB bb8.bb9_crit_edge: Count=0, BFI Count=0
BB bb9: Count=0, BFI Count=0
BB bb9.bb10_crit_edge: Count=0, BFI Count=0
BB bb10: Count=0, BFI Count=0
BB bb10.bb11_crit_edge: Count=0, BFI Count=0
BB bb11: Count=0, BFI Count=0
BB bb12: Count=0, BFI Count=0
BB bb13: Count=0, BFI Count=11
BB bb14: Count=0, BFI Count=10
BB bb15: Count=0, BFI Count=10
BB bb16: Count=0, BFI Count=0
BB bb17: Count=0, BFI Count=0
BB bb18: Count=50, BFI Count=50
BB bb19: Count=50, BFI Count=50
Fixing up profile: Scale=0.906344
```
For all basic blocks that have non‑zero real counts in the profile, the BFI counts are correct. However, because the counts for `bb13`, `bb14`, and `bb15` are positive, the correct counts are incorrectly scaled down by about 10%.
#### Code variant
We also produced a variant of the IR in which all constant branches are replaced with checks on a global variable, so that the lower part of the CFG becomes dynamically dead rather than statically unreachable. This example shows how this issue is not related to unreachable code only, as reachable but never executed code becomes hot too.
```llvm
; ModuleID = '../module_variant.ll'
source_filename = "../module_variant.ll"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
target triple = "x86_64-unknown-linux-gnu"
@b = global i32 3
@c = global i32 1
define void @a() {
bb:
ret void
}
; Function Attrs: noinline
define i16 @func_15() #0 {
bb:
br label %bb1
bb1: ; preds = %bb
br label %bb2
bb2: ; preds = %bb1
%i = load i32, ptr @b, align 4
%i3 = icmp ne i32 %i, 0
br i1 %i3, label %bb4, label %bb5
bb4: ; preds = %bb2
br label %bb18
bb5: ; preds = %bb2
%opaque.6 = load i32, ptr @c, align 4
%cond.6 = icmp ne i32 %opaque.6, 0
br i1 %cond.6, label %bb6, label %bb17
bb6: ; preds = %bb5
store i16 0, ptr null, align 2
br label %bb7
bb7: ; preds = %bb8, %bb6
%opaque.8 = load i32, ptr @c, align 4
%cond.8 = icmp eq i32 %opaque.8, 0
br i1 %cond.8, label %bb8, label %bb16
bb8: ; preds = %bb9, %bb7
%opaque.9 = load i32, ptr @c, align 4
%cond.9 = icmp eq i32 %opaque.9, 0
br i1 %cond.9, label %bb9, label %bb7
bb9: ; preds = %bb10, %bb8
%opaque.10 = load i32, ptr @c, align 4
%cond.10 = icmp eq i32 %opaque.10, 0
br i1 %cond.10, label %bb10, label %bb8
bb10: ; preds = %bb11, %bb9
%opaque.11 = load i32, ptr @c, align 4
%cond.11 = icmp eq i32 %opaque.11, 0
br i1 %cond.11, label %bb11, label %bb9
bb11: ; preds = %bb13, %bb10
%opaque.12 = load i32, ptr @c, align 4
%cond.12 = icmp eq i32 %opaque.12, 0
br i1 %cond.12, label %bb12, label %bb10
bb12: ; preds = %bb11
call void @a()
br label %bb13
bb13: ; preds = %bb15, %bb12
%opaque.14 = load i32, ptr @c, align 4
%cond.14 = icmp eq i32 %opaque.14, 0
br i1 %cond.14, label %bb14, label %bb11
bb14: ; preds = %bb13
br label %bb15
bb15: ; preds = %bb14
br label %bb13
bb16: ; preds = %bb7
ret i16 0
bb17: ; preds = %bb5
ret i16 0
bb18: ; preds = %bb4
br label %bb19
bb19: ; preds = %bb18
ret i16 0
}
define i16 @main() {
bb:
%i = call i16 @func_15()
ret i16 %i
}
attributes #0 = { noinline }
```
We run the same pipeline as before, again disabling `fixFuncEntryCount`:
```bash
opt module-variant.ll -passes='pgo-instr-use' -pgo-fix-entry-func=false -pgo-test-profile-file=module.profdata -S -o profiled-variant.ll
```
```bash
opt -passes='print' -disable-output profiled-variant.ll
```
which yields:
```
Printing analysis results of BFI for function 'func_15':
block-frequency-info: func_15
- bb: float = 1.0, int = 18014398509481984, count = 50
- bb1: float = 1.0, int = 18014398509481984, count = 50
- bb2: float = 1.0, int = 18014398509481984, count = 50
- bb4: float = 1.0, int = 18014398501093376, count = 50
- bb5: float = 0.00000000046566, int = 8388608, count = 0
- bb6: float = 0.00000000029104, int = 5242880, count = 0
- bb7: float = 0.0000000093132, int = 167772160, count = 0
- bb7.bb8_crit_edge: float = 0.0000000090222, int = 162529280, count = 0
- bb8: float = 0.00000028871, int = 5200936960, count = 0
- bb8.bb9_crit_edge: float = 0.00000027969, int = 5038407680, count = 0
- bb9: float = 0.00000895, int = 161229045790, count = 0
- bb9.bb10_crit_edge: float = 0.0000086703, int = 156190638109, count = 0
- bb10: float = 0.00027745, int = 4998100419490, count = 0
- bb10.bb11_crit_edge: float = 0.00026878, int = 4841909781381, count = 0
- bb11: float = 0.00078953, int = 14222975782878, count = 0
- bb12: float = 0.00052075, int = 9381066001496, count = 0
- bb13: float = 0.016664, int = 300194112047877, count = 1
- bb14: float = 0.016143, int = 290813046046381, count = 1
- bb15: float = 0.016143, int = 290813046046381, count = 1
- bb16: float = 0.00000000029104, int = 5242880, count = 0
- bb17: float = 0.00000000017462, int = 3145728, count = 0
- bb18: float = 1.0, int = 18014398501093376, count = 50
- bb19: float = 1.0, int = 18014398501093376, count = 50
```
As in the original example, the innermost loop blocks `bb13`, `bb14`, and `bb15` again receive a non‑zero frequency and a positive count, but now their frequencies are slightly higher (0.016 vs 0.013/0.012). The difference is explained by the changed BPI heuristics: in this variant, the out‑edges from `bb5` get probabilities of 62.5% and 37.5%, whereas in the original code they were split 50%/50%. These probabilities come from a different heuristic that applies to the new form of the branch condition and, in turn, increase the overall scaling factor applied by BFI.
Contributor guide
Research direction
Start with llvm/lib/Analysis/BlockFrequencyInfoImpl.cpp and llvm/lib/Analysis/BranchProbabilityInfo.cpp, then inspect fixFuncEntryCount in llvm/lib/Transforms/Instrumentation/PGOInstrumentation.cpp. Reproduce with the provided module.profdata using the opt commands and compare the BFI output for func_15. Done means unreachable or never-executed blocks retain zero counts and fixFuncEntryCount no longer rescales the real profile counts because of them.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100