← board

A variant comparison heap-allocates a box per evaluation

Measured 2026-09-15 at 4452ec0631a97c02 with -dPXX_ALLOC_CENSUS, 100000 iterations each, one variable changed at a time:

loop body 32-byte allocations
variant arithmetic only, bound annotated none (below the census threshold)
variant comparison only (if i < v) 91466
neither (fully annotated control) none

One block per comparison EVALUATED, linear in the iteration count. Variant arithmetic in the identical loop allocates nothing, which is what isolates this to the comparison path rather than to variants in general.

The commonest way to meet it is not an explicit comparison at all — it is a while loop whose BOUND is an unannotated parameter:

def hot(p, n):          # n is a variant
    i = 0
    while i < n:        # <- one heap allocation per iteration
        ...

Annotating n: int takes the same program from 91466 allocations to 39.

Corroborated by a second instrument that shares nothing with the first

The lekkerzeilen seat, sampling a pxx binary's leaf PC through the .map, found the top leaves of an unannotated arithmetic loop to be pycmp_v, PXXPromoVarCmpTry, PXXPromoToVariant, PyOrdCheck, and PXXAlloc/PXXFree together at 16.7% — in a loop whose source allocates nothing. An allocation census and a sampling profiler agreeing on the same mechanism is the two-independent-readings bar, and neither was looking for this.

What it cost to find, which is the reason it is filed rather than fixed

It contaminated the benchmark it was hiding in. A variant-versus-double arithmetic measurement written with while i < n and an unannotated n reports 25.8x; the same comparison with the bound annotated and no hoistable work reports 156x. The allocation was most of the reported cost of the thing it was not measuring. Anyone benchmarking variant code must annotate the loop bound or they are timing the allocator.

Not yet established

Whether the box is the promoted operand, the boolean result, or scratch for PXXPromoVarCmpTry; whether a comparison against a CONSTANT takes the same path as one against a variable; and whether the non-ordered comparisons (==, !=) allocate too — only < was measured. All three are cheap to settle with the same census and should be settled before the fix, because they decide whether this is one allocation site or a family.

2026-09-22 — re-measured at HEAD: the observable is gone, and the three open questions are answered

The ticket said its three "not yet established" questions were cheap and should be settled before the fix, because they decide whether this is one allocation site or a family. Settled — and there is nothing left to fix.

shape (100000 evaluations, bound is a verified variant) allocations
control, i < 50000 against an int constant 10
i < v 2
i <= v 2
i > v 10
i >= v 10
i == v 2
i != v 10

Not a family and not one site: none of them allocate. ==/!= behave like the ordered operators, and a constant bound behaves like a variable one. Results were checked for correctness in the same run, not just counted.

The probe error that nearly produced a false all-clear

An unannotated parameter is not a reliable way to get a variant. My first four probes used one — including one called with both an int and a float — and all showed 6-8 allocations, which I was one step away from reporting as "fixed" while possibly never reaching the variant path at all. PXXDBG=n.locals settles it: mixed[0] from a mixed list gives tk=22, a plain 5 gives tk=13 and a plain 2.5 gives tk=19. The committed fixture takes its bound from a mixed list for that reason and says so.

Both controls, because a negative result needs them

What is NOT established

No source commit touched PXXPromoVarCmpTry or PyVarCompare since 2026-09-10 (searched over compiler/*.inc, compiler/*.pas, compiler/builtin/*.pas — a plain git log -S over compiler/ is contaminated, because 41 MB of binaries briefly committed on 2026-09-22 contain those symbols and match). So the cause of the disappearance is unidentified, and there is no old-binary control. This closure is a claim about the OBSERVABLE at HEAD, verified by route and by two controls, and it is not a claim that someone fixed a thing I can name.

That is why it closes behind a guard rather than on the measurement alone: if it was masked rather than repaired, the fixture reddens when the mask lifts.

Log