Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/benchmark-history.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ env:
*OperatorBenchmarks*LengthTimesLength
*VectorBenchmarks*.Length
*ComparisonBenchmarks*CompareToInterface
*AbstractionCostBenchmarks*
# Short runs: three iterations is enough for a trend line, and a release should not tie up a
# runner for half an hour.
BENCHMARK_JOB: short
Expand Down
10 changes: 7 additions & 3 deletions Semantics.Benchmarks/AbstractionCostBenchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,13 @@ public class AbstractionCostBenchmarks<T>
private T step;
private T other;

private Length<T> seedLength;
private Length<T> stepLength;
private Length<T> otherLength;
// Assigned in GlobalSetup before anything is measured. Initialised here because a quantity
// was a class before 4.0, where an unassigned field is a null reference the compiler rejects;
// from 4.0 it is a record struct and this is simply its default. The backfill measures those
// releases too, so the file has to compile against both shapes.
private Length<T> seedLength = default!;
private Length<T> stepLength = default!;
private Length<T> otherLength = default!;

/// <summary>
/// Prepares the operands, the bare ones and the wrapped ones holding the same values.
Expand Down
29 changes: 26 additions & 3 deletions Semantics.Benchmarks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,32 @@ Each pair runs identical arithmetic on `T` and on quantities over `T`, with the
| `decimal` | 1.04 | 0.90 | none either side |
| `PreciseNumber` | 1.03 | 1.03 | 193 B either side, ratio 1.00 |

The wrapper is free. Read the ratios below 1.00 as noise and code layout rather than as the quantity
being faster than the number inside it — the spread across three short-run iterations covers that
much, and there is no mechanism by which it could be.
The wrapper is free. A ratio below 1.00 is **not** the quantity beating the number inside it —
wrapping cannot remove work, and there is no mechanism by which it could. Where such a ratio is
stable rather than scattered, and `decimal` multiply is stable at about 0.86 across 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. Read the whole column as "no
measurable wrapper cost" rather than as a direction.

### It was not always free, and the chart says when

`docs/benchmarks/` now carries this per release, and the answer before 4.0 is a different one
entirely. In 3.3.1, when a quantity was a class rather than a `readonly record struct`:

| storage | `Add` | `Multiply` |
|---|---|---|
| `double` | **27.84** | **56.37** |
| `decimal` | — | 2.27 |
| `PreciseNumber` | — | 1.33 |

Every operation allocated an object, so a `Length<double>` add cost 28 times a bare `double` add.
4.0 took that to 1.00 and six releases have held it there.

The spread across that row is the reason this suite is parameterised by storage type at all: 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.

The `PreciseNumber` row is the one that says it most precisely, because it is the only storage type
here that allocates at all: **the allocation ratio is exactly 1.00**. Every byte belongs to the
Expand Down
Loading
Loading