Skip to content

Chart the quantity wrapper's cost per release - #246

Merged
matt-edmondson merged 3 commits into
mainfrom
claude/magical-knuth-idzj5r
Sep 17, 2026
Merged

matt-edmondson merged 3 commits into
mainfrom
claude/magical-knuth-idzj5r

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

AbstractionCostBenchmarks already measured what a quantity costs over the bare storage type it wraps. This draws it per release, as a third chart section, so the answer is watched rather than taken once.

The same section, from the same shared tool, is now in ktsu.PreciseNumber (#85, merged) and ktsu.SignificantNumber (#91, merged). There the pair being divided is that library's type against a bare double, and the ratio is the price of arbitrary precision — well above 1, rightly so. Here the divisor is the storage type the quantity holds, so this section belongs at 1 and a departure from it is the finding. The captions say which is which.

the chart as it stands

What ten releases say

release Add (double) Multiply (double) Multiply (decimal) Multiply (precise)
3.3.1 27.84 56.37 2.27 1.33
4.0.0 1.02 0.93 0.86 1.04
4.1.0 1.00 0.93 0.86 1.03
4.2.0 0.99 0.92 0.85 1.02
4.3.2 1.00 0.94 0.86 1.05
5.0.0 1.01 0.93 0.91 1.02
5.1.0 1.00 0.94 0.87 1.05
5.2.0 1.01 0.94 0.86 1.02
5.2.4 1.00 0.93 0.87 0.98
5.3.2 0.99 0.95 0.86 1.03

4.0 is where the abstraction became free. Before it a quantity was a class, so every operation allocated an object and a Length<double> add cost 28 times a bare double add. Six releases since have held it at 1.

The 3.3.1 row is the argument for the storage-type axis

The same wrapper, in the same release, cost 56× over a double and 1.33× over a PreciseNumber. An allocation per operation is crushing when T is a machine instruction and invisible when T is already doing arbitrary-precision arithmetic.

Measured at one storage type, the 4.0 change would have looked like anything between a rewrite and a rounding error depending which one you picked. That is why this suite is parameterised by storage type rather than measured at a single representative.

On ratios below 1

A ratio below 1 is not the quantity beating the number inside it — wrapping cannot remove work. Where such a ratio is stable rather than scattered, and decimal multiply sits at about 0.86 in every release measured, it is code layout: the two loops compile to slightly different orderings and the wrapped one happens to land better for that type's software multiply. The column reads as "no measurable wrapper cost", not as a direction.

I had earlier described these as noise; ten releases of 0.85–0.91 is too stable for that, and the sharper reading is above.

Two other things in here

  • Four panels rather than two, because the storage type is this library's axis: Add over double, then Multiply over double, decimal and PreciseNumber. Multiply is the one to watch, being the generated relationship that lands on another dimension.
  • = default! on the quantity fields. The backfill now runs this class, and before 4.0 a quantity was a class, where an unassigned field is a null reference the compiler rejects. The other benchmark classes already carried this; AbstractionCostBenchmarks was added after them and was not in the backfill filter until now, so 3.3.1 was the first version to ask it to compile — and it failed, which is how this was found.

The chart section

It derives its ratio from two benchmarks already stored rather than recording a new field. So there is no schema change, no history to rewrite, and an entry gathered before the pair existed simply draws as "not measured".

Verification

  • dotnet build -c Release — clean, 0 warnings.
  • dotnet test -c Release — 1275 passed, 0 failed, 8 skipped (Windows-only path tests).
  • The table above is a real local seeding run: all 10 releases measured in one pass against a single reference reading, so the ratios are comparable as they stand.

One error in the local dotnet test run is not from this change: Semantics.Cpp.Test multi-targets net10.0;net9.0 as it does on main, and the container has only the .NET 10 runtime, so the net9.0 pass cannot start. CI installs both.

🤖 Generated with Claude Code

https://claude.ai/code/session_017jrnV7N94UGL8fDRRE8Xt8


Generated by Claude Code

AbstractionCostBenchmarks already measured what a quantity costs over the bare
storage type it wraps. This draws that per release, as a third chart section, so
the answer is watched rather than taken once.

The same section, from the same shared tool, goes into ktsu.PreciseNumber and
ktsu.SignificantNumber, where the pair being divided is that library's type
against a bare double. Here the divisor is the storage type the quantity holds,
which is why this caption reads differently: a quantity holds one value and adds
no work of its own, so this section belongs at 1 and a departure from it is the
finding. There the ratio is the price of arbitrary precision, well above 1 and
rightly so, and what matters is that it stays put.

Four panels rather than two, because the storage type is this library's axis:
Add over double, then Multiply over double, decimal and PreciseNumber. Multiply
is the one to watch, being the generated relationship that lands on another
dimension.

The section derives its ratio from two benchmarks already stored rather than
recording a new field, so there is no schema change, no history to rewrite, and
an entry gathered before the pair existed simply draws as not measured.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017jrnV7N94UGL8fDRRE8Xt8
…tch]

The backfill now runs this class, and before 4.0 a quantity was a class, where
an unassigned field is a null reference the compiler rejects. Same default!
the other benchmark classes already carry; this one was added after them and
was not in the backfill filter until now, so 3.3.1 was the first version to
ask it to compile.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017jrnV7N94UGL8fDRRE8Xt8
Ten releases measured in one pass against a single reference reading, so the
ratios are comparable as they stand. The wrapper over the bare storage type:

            add(dbl)  mul(dbl)  mul(dec)  mul(prec)
    3.3.1      27.84     56.37      2.27       1.33
    4.0.0       1.02      0.93      0.86       1.04
    4.3.2       1.00      0.94      0.86       1.05
    5.0.0       1.01      0.93      0.91       1.02
    5.3.2       0.99      0.95      0.86       1.03

4.0 is where the abstraction became free. Before it a quantity was a class, so
every operation allocated an object and a Length<double> add cost 28 times a
bare double add; six releases since have held it at 1.

The spread across the 3.3.1 row is why this suite is parameterised by storage
type rather than measured at one. The same wrapper, in the same release, cost
56x over a double and 1.33x over a PreciseNumber: an allocation per operation
is crushing when T is a machine instruction and invisible when T is already
doing arbitrary-precision arithmetic. Measured at one storage type the 4.0
change would have looked like anything between a rewrite and a rounding error.

A ratio below 1 is not the quantity beating the number inside it; wrapping
cannot remove work. Where it is stable rather than scattered, and decimal
multiply sits at about 0.86 in every release, it is code layout rather than
noise -- the two loops compile to slightly different orderings and the wrapped
one lands better for that type's software multiply. The column reads as no
measurable wrapper cost, not as a direction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017jrnV7N94UGL8fDRRE8Xt8
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants