Withdraw the fused multiply-add, and measure what the grid costs
The section claimed what was left of fixed point's cost was "an integer multiply with a rounding against a float fused multiply-add". Both halves are wrong and the evidence is in the binaries. `5ed9e87` contains no FMA instruction, and rebuilding it with `-C target-feature=+fma,+avx2` still produces none: Rust does not contract `a + b * c`, so `LerpUtil::lerp` is `subss`, `mulss`, `addss` on any target. The float side never had the instruction the grid was being compared against. Nor is the multiply the cost. Replacing `shift_round` in `Fixed::mul` with a truncating shift -- all twenty-seven work counters diffed identical -- takes 43M instructions off and leaves cycles inside the noise. What moves cycles is whether `UiSpan::within` inlines: the rounding and both short-circuits removed together are worth 118M instructions and 57M cycles where separately they are worth 61M and 8M, and `<UiSpan>::within` stops being a symbol. None of that is shippable, but it is where to push. Three other entries corrected against measurement rather than argument. The division is not 0.00% of cycles -- that was `apply_scalar`; `Holds::through` runs 2.23M `i64` divisions a 500-frame `many` and holds the divider busy 21.3M cycles. `d75a1e2` and `4cbb242`'s short-circuits no longer earn anything now that `Fixed` wraps. Branchless `shift_round` still loses, for the opposite reason to the one recorded: it is the rounding's size, not its branch. And the measurement posture: cycles spread 1-3% across nine runs of one binary rather than 0.23%, `instructions:u` returns a garbage value in roughly a quarter of readings, and an instruction count moves 0.5% across a rebuild of identical source. Quote deltas against a baseline built beside the thing measured. Checked: `/home/bob/repos/iris-float-cmp` carries `5ed9e87` with `Edits::fixed_branches` applied, and is recorded under "Rigs and reproduction" so the comparison can be re-run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
663a95f4a2
commit
10e46522b4
1 file changed
+118
-24
+118
-24
@@ -163,29 +163,100 @@ Measured on the fixed-shape fixture, seed 1, depth 8, 500 frames of `many`:
|
|||||||
|
|
||||||
| | drawn widgets | primitive writes | instructions | cycles |
|
| | drawn widgets | primitive writes | instructions | cycles |
|
||||||
| --- | ---: | ---: | ---: | ---: |
|
| --- | ---: | ---: | ---: | ---: |
|
||||||
| `5ed9e87`, before fixed point | 100 | 4,272 | 1,761M | ~699M |
|
| `5ed9e87`, before fixed point | 100 | 4,272 | 1,763M | 693M |
|
||||||
| `4cbb242` | 97 | 3,951 | 2,093M | ~819M |
|
| `4cbb242` | 97 | 3,951 | 2,093M | ~819M |
|
||||||
| `4febabf`, wrapping | 97 | 3,951 | 1,918M | ~771M |
|
| `4febabf`, wrapping | 97 | 3,951 | 1,912M | 771M |
|
||||||
|
|
||||||
Normalised by primitive writes the grid is now about 1.18x the float head
|
The two ends were re-measured 2026-09-16 as medians of nine runs of two
|
||||||
on instructions and 1.19x on cycles; per drawn widget it is 1.12x. What is
|
freshly built binaries and are the numbers above; `4cbb242`'s row is still
|
||||||
left is the shape of an integer multiply with a rounding against a float
|
the single reading it was taken from. Raw, the head is 1.085x the float head
|
||||||
fused multiply-add, spread across `UiSpan::within` (4.7% of cycles),
|
on instructions and 1.113x on cycles. Normalised by primitive writes it is
|
||||||
`remap_subtree` and `Holds::through`; nothing in the profile above those
|
1.17x and 1.20x; per drawn widget 1.12x. **Take none of those for the grid's
|
||||||
is the grid. Text line breaking is 10% and `InstanceList::push` 9%.
|
cost.** The two heads do different work rather than the same work at
|
||||||
|
different speeds -- `4febabf` remaps 53 subtrees a frame and redraws 27
|
||||||
|
where `5ed9e87` remaps 38 and redraws 43 -- and dividing by primitive writes
|
||||||
|
does not correct for a different mix of remapping and redrawing.
|
||||||
|
|
||||||
**Cycles are measurable on this machine and instructions alone are not
|
**The fused multiply-add this section used to name does not exist,
|
||||||
enough.** Three runs of one binary varied 0.23%. It is *wall time* that
|
withdrawn 2026-09-16.** `5ed9e87`'s binary holds no FMA instruction at all,
|
||||||
varies 2x here, which is what the machine notes say; the counters are
|
and none appears when it is rebuilt with `-C target-feature=+fma,+avx2`
|
||||||
steady. IPC is 2.58 before fixed point and 2.59 after, so the cost is
|
either. Rust does not contract `a + b * c`, so `LerpUtil::lerp`'s
|
||||||
instruction count at unchanged throughput -- not stalls, not branch misses
|
`from + (to - from) * self` is `subss`, `mulss`, `addss` on every target
|
||||||
(0.69%), not the division.
|
there is. The float side never had the single instruction the grid's
|
||||||
|
multiply was being compared against, here or anywhere.
|
||||||
|
|
||||||
**What the cycle profile says the cost is**, against what the previous
|
**Nor is the multiply the cost.** Each row below is `4febabf` with one
|
||||||
session recorded from the disassembly. In `AxisRemap::apply_scalar` the
|
thing taken out, in one worktree so the rows compare to each other, 500
|
||||||
`i64` division is **0.00% of cycles** and the multiply 1.5%: the time is in
|
frames of `many` at seed 1 depth 8, medians of nine to eleven runs. **Every
|
||||||
`saturating_add`, which is five instructions and has no `i32` vector form.
|
row leaves the work counters where they were**, so unlike the cross-commit
|
||||||
The division is rare because most moves are translations.
|
numbers above these compare one tree against itself: all twenty-seven
|
||||||
|
counters diffed identical for the all-three row that the conclusion rests
|
||||||
|
on, and six of them checked for the others.
|
||||||
|
|
||||||
|
| | instructions | cycles |
|
||||||
|
| --- | ---: | ---: |
|
||||||
|
| `4febabf` | 1,921M | 772M |
|
||||||
|
| `shift_round` out of `Fixed::mul`, truncating instead | 1,878M | 774M |
|
||||||
|
| `is_full` short-circuits out of `UiSpan::within` | 1,910M | 765M |
|
||||||
|
| zero short-circuit out of `Fixed::scaled` | 1,914M | 768M |
|
||||||
|
| all three | **1,803M** | **715M** |
|
||||||
|
| branchless `shift_round`, both short-circuits out | 1,964M | 773M |
|
||||||
|
|
||||||
|
Taking the rounding out alone removes 43M instructions and moves cycles by
|
||||||
|
+2M, which is inside the noise: the grid's arithmetic is instructions the
|
||||||
|
machine has spare issue width for, not time.
|
||||||
|
|
||||||
|
**IPC has reversed since this section recorded it**, and that is the same
|
||||||
|
finding from the other side. It was 2.58 before fixed point and 2.59 after,
|
||||||
|
which is what "the cost is instruction count at unchanged throughput" was
|
||||||
|
read from. On the re-measured medians it is **2.54 before and 2.48 after**:
|
||||||
|
the head both executes more instructions and retires them more slowly, so
|
||||||
|
there is something to find beyond counting them. The all-three build
|
||||||
|
recovers half of it against its own worktree's baseline, 2.49 to 2.52.
|
||||||
|
|
||||||
|
**What moves cycles is whether `UiSpan::within` inlines, and the three cuts
|
||||||
|
are superadditive because of it.** Separately they are worth 61M
|
||||||
|
instructions and 8M cycles between them; together, 118M and 57M -- four
|
||||||
|
fifths of the instruction gap and two thirds to three quarters of the cycle
|
||||||
|
gap, which is the looser number. The mechanism is visible in `nm`:
|
||||||
|
`<UiSpan>::within` is a symbol in `4febabf` and in neither `5ed9e87` nor the
|
||||||
|
all-three build. Shortening its body past the inliner's threshold is what
|
||||||
|
pays, and only the whole set of cuts reaches it. None of them is shippable
|
||||||
|
-- truncating breaks the one rounding the grid is for -- but that is where
|
||||||
|
to push, and a cheaper `Holds` or a smaller `Len::within` is worth trying
|
||||||
|
for this reason rather than for its own instruction count.
|
||||||
|
|
||||||
|
**Cycle noise is larger than this section used to claim.** Across ten sets
|
||||||
|
of nine to eleven runs, each of one unchanged binary, cycles spread 1-3%
|
||||||
|
usually and 6.7% in the worst set -- not the 0.23% recorded from three
|
||||||
|
runs, which was a lucky set read as the machine's precision. Worse,
|
||||||
|
`instructions:u` in this VM intermittently returns a garbage value
|
||||||
|
-- 11.0e9, 25.8e9 and 28.4e9 against a true 1.9e9, in roughly a quarter of
|
||||||
|
readings. Take medians of nine or more, drop anything off by a factor, and
|
||||||
|
do not trust a single `perf stat`. `ex_div_busy` was the one counter that
|
||||||
|
held to 0.1% across runs.
|
||||||
|
|
||||||
|
**Instruction counts hold to 0.02% within a binary and move 0.5% across a
|
||||||
|
rebuild of the same source**, which is why the two tables above disagree
|
||||||
|
about `4febabf` by 9M: their baselines were built in different worktrees.
|
||||||
|
The *difference* against `5ed9e87` was 149M in both builds. So compare
|
||||||
|
against a baseline built beside the thing being measured, quote a delta
|
||||||
|
rather than an absolute, and re-measure the baseline whenever anything is
|
||||||
|
rebuilt.
|
||||||
|
|
||||||
|
**"The division is 0.00% of cycles" was read off the wrong function.** It
|
||||||
|
is true of `AxisRemap::apply_scalar`, where most moves are translations and
|
||||||
|
the multiply is 1.5%. It is not true of the run: `Holds::through` divides
|
||||||
|
twice on every call and accounts for essentially all of the 2.23M `i64`
|
||||||
|
divisions a 500-frame `many` executes, which hold the integer divider busy
|
||||||
|
**21.30M cycles** -- the steadiest counter on this machine, 0.1% across
|
||||||
|
runs, and 2.8% of the run. A controlled extra `div_toward` in `through`,
|
||||||
|
against a de-const control so the inlining does not move, costs 6.3 cycles
|
||||||
|
each, so the two real ones are about 14M. `5ed9e87` divides twice there too,
|
||||||
|
in floats, so this is a real cost but a smaller *difference* than the
|
||||||
|
source-line profile makes it look, where `div_toward`'s `num / den` is the
|
||||||
|
largest single grid line in the head at 106M of 5,361M over 4,000 frames,
|
||||||
|
against `shift_round`'s 71M.
|
||||||
|
|
||||||
Recovered this session, each measured before and after on one commit with
|
Recovered this session, each measured before and after on one commit with
|
||||||
byte-identical renders proving the layout unchanged -- on the old fixture,
|
byte-identical renders proving the layout unchanged -- on the old fixture,
|
||||||
@@ -197,21 +268,34 @@ byte-identical renders proving the layout unchanged -- on the old fixture,
|
|||||||
translation is: -4% instructions, -3.4% cycles.
|
translation is: -4% instructions, -3.4% cycles.
|
||||||
- `d75a1e2` and `4cbb242` skip multiplies that cannot change anything --
|
- `d75a1e2` and `4cbb242` skip multiplies that cannot change anything --
|
||||||
composing through a full box, and `lerp` where the ends are equal:
|
composing through a full box, and `lerp` where the ends are equal:
|
||||||
together -2.7% instructions, -1.8% cycles.
|
together -2.7% instructions, -1.8% cycles when they landed. **That no
|
||||||
|
longer reproduces at `4febabf`**: removing either one now *saves* a
|
||||||
|
little, and removing both is part of the 118M/57M above. Wrapping changed
|
||||||
|
what a multiply costs, and a guard priced against a saturating multiply is
|
||||||
|
priced against something that is gone. Re-price a short-circuit before
|
||||||
|
keeping it.
|
||||||
|
|
||||||
Tried and rejected, with numbers, so they are not tried again:
|
Tried and rejected, with numbers, so they are not tried again:
|
||||||
|
|
||||||
- A float reciprocal for the division: **+6% cycles**. The conversions cost
|
- A float reciprocal for the division: **+6% cycles**. The conversions cost
|
||||||
more than the `idiv` that is not on the hot path anyway.
|
more than the `idiv` did there. That was `apply_scalar`'s division; the
|
||||||
- Branchless `shift_round`: **+6.7% cycles**. The sign of a product predicts
|
one in `Holds::through` is a different question and has not been tried.
|
||||||
well, and the positive path is two instructions.
|
- Branchless `shift_round`: **+6.7% cycles** when tried alone, and
|
||||||
|
re-tested 2026-09-16 with both short-circuits also removed, where the
|
||||||
|
branchy form reaches 1,803M/715M: 1,964M instructions and 773M cycles,
|
||||||
|
worse than the head on both. It is the *size* of the rounding that keeps
|
||||||
|
`within` out of line, not its branch, so making it branchless makes it
|
||||||
|
bigger and loses twice.
|
||||||
- Removing the per-child hash lookup in `remap_subtree` with `mem::take`:
|
- Removing the per-child hash lookup in `remap_subtree` with `mem::take`:
|
||||||
**0.0%**. `FxHashMap` on a hot line is not the cost.
|
**0.0%**. `FxHashMap` on a hot line is not the cost.
|
||||||
- Short-circuiting `apply_scalar` where the fraction is nought or one: +17%.
|
- Short-circuiting `apply_scalar` where the fraction is nought or one: +17%.
|
||||||
(From the previous session.)
|
(From the previous session.)
|
||||||
|
|
||||||
- `#[inline]` on `UiSpan::within`, whose prologue and epilogue were a sixth
|
- `#[inline]` on `UiSpan::within`, whose prologue and epilogue were a sixth
|
||||||
of its own cycles: -0.2% instructions, **+1.5% cycles**.
|
of its own cycles: -0.2% instructions, **+1.5% cycles**. That is not in
|
||||||
|
tension with the inlining finding above: forcing the body it has now into
|
||||||
|
every caller loses, and shrinking the body until the inliner takes it of
|
||||||
|
its own accord wins. Shrink it; do not annotate it.
|
||||||
|
|
||||||
**`4febabf` makes `Fixed` wrap rather than saturate** (Bryan, 2026-09-16: a
|
**`4febabf` makes `Fixed` wrap rather than saturate** (Bryan, 2026-09-16: a
|
||||||
coordinate past the range is not going to draw reasonably anyway, so wrap
|
coordinate past the range is not going to draw reasonably anyway, so wrap
|
||||||
@@ -541,6 +625,16 @@ same state. `IRIS_GENERATED_SEED`, `IRIS_GENERATED_SEEDS`, and
|
|||||||
tree; use it to turn a seed into a readable regression rather than leaving the
|
tree; use it to turn a seed into a readable regression rather than leaving the
|
||||||
seed as the only record.
|
seed as the only record.
|
||||||
|
|
||||||
|
**The float head is checked out at `/home/bob/repos/iris-float-cmp`**, at
|
||||||
|
`5ed9e87` with `394d514`'s `Edits::fixed_branches` cherry-applied to
|
||||||
|
`src/random.rs` and `tests/layout_diagnostics.rs` but not committed, since
|
||||||
|
the flag is the only way to measure the two on one tree. Build it and
|
||||||
|
`iris-pr18` with `--test layout_diagnostics` and run the binaries directly
|
||||||
|
rather than through cargo. A comparison is only worth reading when the
|
||||||
|
`--features layout-diagnostics` counters match on both sides; check
|
||||||
|
`primitive writes`, `widget draws`, `reuse remapped` and `placed by
|
||||||
|
redrawing` before any timing.
|
||||||
|
|
||||||
The headless reference set must be run one process at a time because the rig
|
The headless reference set must be run one process at a time because the rig
|
||||||
reuses one compositor. Comparison worktrees need separate target directories.
|
reuses one compositor. Comparison worktrees need separate target directories.
|
||||||
Useful commands:
|
Useful commands:
|
||||||
|
|||||||
Reference in new issue
Block a user