Chart the quantity wrapper's cost per release - #246
Merged
Merged
Conversation
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
|
This was referenced Sep 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



AbstractionCostBenchmarksalready 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) andktsu.SignificantNumber(#91, merged). There the pair being divided is that library's type against a baredouble, 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.What ten releases say
Add(double)Multiply(double)Multiply(decimal)Multiply(precise)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 baredoubleadd. 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
doubleand 1.33× over aPreciseNumber. An allocation per operation is crushing whenTis a machine instruction and invisible whenTis 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
decimalmultiply 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
Addoverdouble, thenMultiplyoverdouble,decimalandPreciseNumber.Multiplyis 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;AbstractionCostBenchmarkswas 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).One error in the local
dotnet testrun is not from this change:Semantics.Cpp.Testmulti-targetsnet10.0;net9.0as it does onmain, 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