On Mon, Aug 24, 2026 at 8:40 PM <[email protected]> wrote: > > From: Kyrylo Tkachov <[email protected]> > > branch_prob () cannot place a counter on an abnormal edge. It > represents the flow of a computed goto with fake edges. One fake edge > goes from the entry block to each target. Another goes from the > dispatch block to the exit block. branch_prob () also marks the > abnormal edges as ignored. An ignored edge that is not on the spanning > tree is not instrumented. The graph solver does not infer its count. > Its count therefore remains at the default zero, whatever the program > did. > > compute_branch_probabilities () then turns that zero into a precise > never. A precise never is proof of deadness. > propagate_unlikely_bbs_forward () cannot reach any dispatch target from > the entry block, and determine_unlikely_bbs () zeroes the measured > counts of the whole region. For a threaded interpreter that region is > the entire hot loop. Thus, -fprofile-use reads measured block counts > and then discards them. > > For > > static const void *tab[] = { &&L_ADD, ... }; > goto *tab[*pc++]; > > -fdump-ipa-profile shows 5000 executions for the dispatch block and 1000 > for each handler. The optimized dump then reads > > ;; basic block 5, loop depth 1, count 0 (precise), probably never executed > > for every one of them. > > Record the probability of an ignored edge outside the spanning tree as a > guess. A guessed never is still a zero probability. It does not invent > a split between the dispatch targets. A target with a measured zero > count also stays at zero. The guessed probability stops the unmeasured > edge count from being evidence that the target is dead. The same dump > now reads > > ;; basic block 5, loop depth 1, count 5000 (precise), maybe hot > > The rule is applied per edge and attributes no flow. It does not depend > on how many dispatch blocks a function has. It also needs no notion of > which dispatch a target belongs to. > > Evaluated on aarch64 (NVIDIA Vera) with CPython 3.14.5 at -O3. CPython > was configured with --enable-optimizations and --with-lto. In > Python/ceval.c, the share of the basic blocks of > _PyEval_EvalFrameDefault left with a precise zero count falls from 98.3% > to 23.1%. During the full-program LTO link, the function grows by 0.7% > and the interpreter text grows by 0.1%. A fixed-work > pyperformance-derived workload takes 2.5% fewer cycles. > > Bootstrapped and tested on aarch64-none-linux-gnu.
Wouldn't UNINITIALIZED be what actually happens? We simply don't know due to lack of counters. Or do we ICE on that later, requiring sth else than UNINITIALIZED? > gcc/ChangeLog: > > * profile.cc (compute_branch_probabilities): Mark probabilities of > ignored edges outside the spanning tree as guessed. > > gcc/testsuite/ChangeLog: > > * gcc.dg/tree-prof/comp-goto-2.c: New test. > > Signed-off-by: Kyrylo Tkachov <[email protected]> > --- > gcc/profile.cc | 5 ++ > gcc/testsuite/gcc.dg/tree-prof/comp-goto-2.c | 61 ++++++++++++++++++++ > 2 files changed, 66 insertions(+) > create mode 100644 gcc/testsuite/gcc.dg/tree-prof/comp-goto-2.c > > diff --git a/gcc/profile.cc b/gcc/profile.cc > index 5ae64d4043f..1683794e5e3 100644 > --- a/gcc/profile.cc > +++ b/gcc/profile.cc > @@ -719,6 +719,11 @@ compute_branch_probabilities (unsigned cfg_checksum, > unsigned lineno_checksum) > bool prev_never = e->probability == profile_probability::never > (); > e->probability = profile_probability::probability_in_gcov_type > (edge_gcov_count (e), bb_gcov_count (bb)); > + /* Ignored edges outside the spanning tree have neither a > counter > + nor an inferred count. Do not treat their zero count as a > + precise never. */ > + if (EDGE_INFO (e)->ignore && !EDGE_INFO (e)->on_tree) > + e->probability = e->probability.guessed (); > if (e->probability == profile_probability::never () > && !prev_never > && flag_profile_partial_training) > diff --git a/gcc/testsuite/gcc.dg/tree-prof/comp-goto-2.c > b/gcc/testsuite/gcc.dg/tree-prof/comp-goto-2.c > new file mode 100644 > index 00000000000..d005b52bc60 > --- /dev/null > +++ b/gcc/testsuite/gcc.dg/tree-prof/comp-goto-2.c > @@ -0,0 +1,61 @@ > +/* The ignored edges out of a computed goto are outside the spanning tree, > + so their zero count used to become a precise "never" and mark every opcode > + handler as dead. Check that the measured counts survive -fprofile-use. > */ > +/* { dg-options "-O2 -fdump-tree-optimized-blocks-details" } */ > +/* { dg-require-effective-target indirect_jumps } */ > +/* { dg-require-effective-target label_values } */ > + > +enum { OP_ADD, OP_SUB, OP_MUL, OP_STORE, OP_HALT, NOPS }; > + > +unsigned char code[8]; > +long mem[4] = { 1, 2, 3, 4 }; > + > +__attribute__((noinline)) long > +interp (const unsigned char *pc) > +{ > + static const void *const tab[NOPS] > + = { &&L_ADD, &&L_SUB, &&L_MUL, &&L_STORE, &&L_HALT }; > + long acc = 0; > + > +#define DISPATCH() goto *tab[*pc++] > + > + DISPATCH (); > +L_ADD: > + acc += mem[*pc++]; > + DISPATCH (); > +L_SUB: > + acc -= mem[*pc++]; > + DISPATCH (); > +L_MUL: > + acc *= 3; > + DISPATCH (); > +L_STORE: > + mem[*pc++] = acc; > + DISPATCH (); > +L_HALT: > + return acc; > +} > + > +int > +main (void) > +{ > + long r = 0; > + int i; > + > + code[0] = OP_ADD; code[1] = 1; > + code[2] = OP_SUB; code[3] = 2; > + code[4] = OP_MUL; > + code[5] = OP_STORE; code[6] = 3; > + code[7] = OP_HALT; > + > + for (i = 0; i < 1000; i++) > + r += interp (code); > + > + return r == 0; > +} > + > +/* No block of the dispatch loop may be left with a precise zero count. */ > +/* { dg-final-use-not-autofdo { scan-tree-dump-not "count 0 \\(precise" > "optimized" } } */ > +/* The ignored non-spanning-tree edges carry no measurement, so they must be > + a guess. */ > +/* { dg-final-use-not-autofdo { scan-tree-dump "never \\(guessed\\)" > "optimized" } } */ > -- > 2.50.1 (Apple Git-155) >
