> > > > On 25 Aug 2026, at 12:36, Richard Biener <[email protected]> wrote: > > > > 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? > > Yes, UNINITIALIZED describes the information we have, but it cannot remain in > that state here. > The earlier profile-estimation pass has already set cfun->cfg->full_profile. > I tested setting these edge probabilities to UNINITIALIZED. The cleanup after > branch_prob () then reports “probability of edge ... not initialized” and > fails with verify_flow_info failed.
The problem with unitialized values is that you can not really drop them meaningfully into the cost model (i.e. estimate cost of a spill in register allocator or benefit of inline). At the end of day we need actual numbers to compute prioriites on. So once profile is build everyone is supposed to fill the unknowns by reasonable guesses. Guessed 0 is kind of dangerous value for computed goto if it lives on a hot path (which computed gotos may). It means that the 0 may get propagated further and code on hot path become cold. Also BB reorder will then not place the computed goto close to its likely target which is probably still useful optimization Honza > > Thanks, > Kyrill
