https://gcc.gnu.org/bugzilla/show_bug.cgi?id=127125

            Bug ID: 127125
           Summary: AutoFDO uses an entry-region position sample as entry
                    flow when the function head count is zero
           Product: gcc
           Version: 17.0
            Status: UNCONFIRMED
          Severity: normal
          Priority: P3
         Component: gcov-profile
          Assignee: unassigned at gcc dot gnu.org
          Reporter: luolongjuna at gmail dot com
  Target Milestone: ---

Created attachment 65443
  --> https://gcc.gnu.org/bugzilla/attachment.cgi?id=65443&action=edit
AutoFDO head=0 profile for the posted repro.c

An AutoFDO profile can contain ordinary source-position samples for a
function whose function-head count is zero.  On the default
`auto-profile-bbs=1` path, GCC can use a sample in an entry-equivalent region
(code executed once for every function invocation) as exact entry flow.  It
then propagates that value through a PHI source location to the edge entering
a loop.  In the testcase below this produces a loop trip estimate below one
even though the loop executes 64 times.

I reproduced this with:

```text
GCC 17.0.0 20260828 (experimental)
commit aaa0f6b1e87bc62ea15da6eab431af6dcebf6414
target x86_64-pc-linux-gnu
```

It also reproduces with Fedora GCC 16.1.1 20260515.

### Reproducer

Download the attached `profile.afdo` and save the following source as
`repro.c` in the same directory.  Do not reformat it: the profile uses
function-relative source-line offsets.

```c
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

__attribute__((noinline, noipa))
static uint64_t worker(const uint32_t *data, unsigned count, uint64_t acc) {
  acc ^= UINT64_C(0x9e3779b97f4a7c15); /* PROFILE_ENTRY */

  for (unsigned i = 0; i < count; ++i) {
    acc += (uint64_t)data[i] * UINT64_C(0x632be59bd9b4e019); /* PROFILE_BODY */
    acc = (acc << 9) | (acc >> (64 - 9));
  }

  return acc;
}

int main(int argc, char **argv) {
  unsigned count = argc > 1 ? (unsigned)strtoul(argv[1], NULL, 10) : 64;
  if (count == 0 || count > 256)
    return 2;

  uint32_t data[256];
  for (unsigned i = 0; i < count; ++i)
    data[i] = i * 2654435761U;

  printf("%" PRIu64 "\n", worker(data, count, 1));
  return 0;
}
```

Compile it from that directory:

```sh
gcc -O2 -g -gdwarf-4 -fno-pie -no-pie \
  -fno-omit-frame-pointer \
  -ffile-prefix-map="$PWD=/repro" \
  -fauto-profile="$PWD/profile.afdo" \
  -fdump-ipa-afdo-details \
  -fdump-tree-loopinit-details \
  "$PWD/repro.c" -o repro

grep -H -m1 'iterations by profile:' ./*.loopinit
grep -E 'worker total:|Annotated bb|probability of edge' ./*.afdo
```

The attached profile has SHA-256:

```text
f50518cbf49d02943c1ba6882cc7634b78b9580fdc5eecf3f57e0d9c589bf3de
```

It uses AutoFDO format version 3; `gcc --print-autofdo-gcov-version` should
print `3`.

The attached `profile.afdo` is a deterministic synthetic reproducer.  It was
generated with unmodified upstream `create_gcov` from two text-profile sample
records; it is not one of the hardware profiles described below.

It decodes as:

```text
worker:/repro/repro.c total:1943500 head:0 timestamp: 0
  1: 1000000
  3.2: 0
  4: 943500
  5: 0
  9: 0
```

The two nonzero entries are encoded as source-position sample counts.
Neither is a function entry count or a flow-consistent edge execution count.
`noipa` only keeps the dump stable by preventing IPA cloning.

The relevant GCC dump is:

```text
worker total:1943500 head:0
Annotated bb 2 with count 1000000
Annotated bb 3 with count 943500
probability of edge 2->4 ... set to always
probability of edge 3->4 ... set to always
probability of edge 4->3 ... set to 48.5%
probability of edge 4->5 ... set to 51.5%
```

The loop dump reports:

```text
iterations by profile: 0.943500
```

This is `943500 / 1000000`, although running `./repro 64` makes the loop
execute 64 times.  Sparse sampling is not expected to recover 64 exactly;
the issue is that an ordinary position sample becomes exact entry flow when
there is no function-head evidence.

This is an optimization-quality issue; I did not observe incorrect program
output.

### Expected behavior

Without a positive function-head count, a position sample in the
entry-equivalent region should not by itself become the function entry or
entry-edge count.  In particular, these non-flow-consistent samples should
not force the sub-unit loop estimate and the corresponding 48.5%/51.5% loop
probabilities.

### GCC implementation path

The behavior comes from `gcc/auto-profile.cc`:

1. `afdo_annotate_cfg` reads `s->head_count ()`, which is zero here.
2. The default path still calls `afdo_set_bb_count` for every real basic
   block.  It assigns 1,000,000 to the first real block and 943,500 to the
   loop body.
3. `afdo_calculate_branch_prob` calls `afdo_unscaled_edge_count`.  A PHI
   argument source location associates the first-block sample with the edge
   entering the loop.
4. CFG propagation treats 1,000,000 as entry/exit flow and 943,500 as
   backedge flow, producing the probabilities above.

This report concerns the default `auto-profile-bbs=1` path.  The alternative
scaling path uses sampled counts to choose a function-wide scale rather than
using their ratios as relative CFG flow, so I am not treating its output as
evidence for this issue.

`function_instance::head_count` also documents `-1` as unavailable, and the
default path likewise tests only for a positive value.  That is a related
code-path observation; the attached testcase covers the zero case.

Using AutoFDO's existing `Symbol::EstimateHeadCount` would select the earliest
position count, 1,000,000, in this testcase.  As a separate control, an
otherwise identical profile with `head:1000000` still produces
`iterations by profile: 0.943500`, so that heuristic does not resolve this
case.  This does not rule out every producer-side policy; the appropriate
fallback and where to apply it remain open for review.

### Validation with hardware perf data

To check that `head:0` is not an artifact of the deterministic attachment, I
also collected three `cycles:u` profiles on hardware using IP-only perf
sampling.  I used `perf record --buildid-all -e cycles:u -c 100003` and
converted the captures with unmodified upstream AutoFDO at
`c9fa188bfada0a0aaa844437506f452eadbba8cf`, using `--gcov_version=3` and
`--use_lbr=false`.  The attached profile above remains the self-contained
reproducer.

The validation program placed a single `cpuid` in an entry-equivalent
straight-line region followed by the same fixed 64-iteration loop.  `cpuid`
concentrated genuine cycle samples at one PC without adding a CFG edge before
the loop.  No samples were lost.  The three captures and the corresponding
estimates from GCC's default path were:

| run | samples | function total/head | entry sample | loop sample | GCC
estimate |
|---|---:|---:|---:|---:|---:|
| r1 | 72,158 | 66,064 / 0 | 33,002 | 17,572 | 0.532453 |
| r2 | 72,303 | 66,466 / 0 | 30,380 | 19,967 | 0.657242 |
| r3 | 72,159 | 66,084 / 0 | 32,816 | 17,288 | 0.526816 |

Each estimate equals the loop/entry sample ratio.  Thus all three real
profiles have `head:0`, nonzero entry and loop samples, and reproduce the
same default-path behavior.

For an IP-only perf capture without branch-stack records,
`branch_count_map` is empty, so directly sampled top-level functions retain
the default zero head count.  The deliberately constructed part of this
experiment is the concentration of an ordinary cycle sample in the
entry-equivalent region, not the missing head count itself.

The three hardware profiles in this section were generated directly from the
raw perf captures and were not edited.  Raw captures, generated profiles, and
GCC dumps are available on request.

### Related reports

I did not find an existing report for this specific entry-flow behavior.
Related reports include PR115088 and PR116743.  PR118581 added the PHI source
location recovery used by this testcase, but did not address missing
function-head evidence.  PR120614 concerns broader AutoFDO performance and
profile-consistency problems.  PR120867 is the AutoFDO meta-bug and is listed
in `Blocks` above.

### Possible next step

I have a small prototype for the zero-head case, but the fallback policy is
still open for review.  I can turn it into a patch if this is the right
direction.

Reply via email to