llvm / llvm/llvm-project

[ProfInfo][BFI] BFI analysis incorrectly computing positive counts for unreachable code

Open
#191,126 0 comments 0 reactions 0 assignees View on GitHub
loopoptim PGO
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.

Image

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.

Image

Contributor guide

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.