diff --git a/.github/workflows/benchmark-history.yml b/.github/workflows/benchmark-history.yml
new file mode 100644
index 0000000..57841c2
--- /dev/null
+++ b/.github/workflows/benchmark-history.yml
@@ -0,0 +1,203 @@
+name: Benchmark History
+
+# Measures a small, fixed set of benchmarks once per release, appends the numbers to a committed
+# history file, and redraws the chart the README shows.
+#
+# Two ways in:
+# * a published release, which measures that version and adds one point;
+# * a manual dispatch listing versions, which measures each of them in ONE job and backfills.
+#
+# A backfill version is measured as a published package, through BenchmarkAgainstVersion: no tag
+# before this workflow carries a benchmark project, so there is no older source to run. A release
+# from now on does carry one, and is measured from its own tag through a worktree -- which is also
+# what keeps the release path off a race, since the package for a tag is not necessarily on
+# nuget.org yet at the moment its release is published.
+#
+# The backfill running as a single job still matters: separate runs land on different CI hosts,
+# and that difference is larger than most releases are. Within one job the points are comparable
+# as they stand; across jobs, BaselineBenchmarks is what ties them together.
+
+on:
+ release:
+ types: [published]
+ workflow_dispatch:
+ inputs:
+ versions:
+ description: "Space-separated released versions of ktsu.Semantics.Quantities to backfill, oldest first"
+ required: false
+ default: "3.3.1 4.0.0 4.1.0 4.2.0 4.3.2 5.0.0 5.1.0 5.2.0 5.2.4 5.3.2"
+ type: string
+
+permissions:
+ contents: write
+
+concurrency:
+ group: benchmark-history
+ cancel-in-progress: false
+
+env:
+ DOTNET_VERSION: "10.0"
+ HISTORY: docs/benchmarks/history.json
+ CHART: docs/benchmarks/performance.svg
+ # Already ignored, and ktsu.Sdk regenerates .gitignore on build so a new entry would not last.
+ RUNS: BenchmarkDotNet.Artifacts
+ # The set drawn in the README. One operation per storage type rather than every operation at one
+ # storage type: a quantity is a value type over T and does almost nothing of its own, so what a
+ # release changes it changes per storage type.
+ HEADLINE_FILTER: >-
+ *ConstructionBenchmarks*FromNauticalMile
+ *UnitConversionBenchmarks*InNauticalMile
+ *OperatorBenchmarks*LengthTimesLength
+ *VectorBenchmarks*.Length
+ *ComparisonBenchmarks*CompareToInterface
+ # 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
+
+jobs:
+ measure:
+ name: Measure and chart
+ runs-on: ubuntu-latest
+ timeout-minutes: 240
+
+ steps:
+ - name: Checkout Repository
+ uses: actions/checkout@v7
+ with:
+ # The default branch, not the released tag: the results are committed back here, and a
+ # release event would otherwise leave the checkout detached at the tag, so the push at
+ # the end would be asking the default branch to move backwards.
+ ref: ${{ github.event.repository.default_branch }}
+ fetch-depth: 0
+
+ - name: Setup .NET SDK ${{ env.DOTNET_VERSION }}
+ uses: actions/setup-dotnet@v6
+ with:
+ dotnet-version: ${{ env.DOTNET_VERSION }}.x
+
+ # Measured from this checkout, and stamped onto every entry this job produces: everything
+ # here shares one runner, so one reading of that runner describes all of it.
+ - name: Measure the reference workload
+ id: baseline
+ shell: bash
+ run: |
+ set -euo pipefail
+ dotnet run -c Release --project Semantics.Benchmarks -- \
+ --filter '*BaselineBenchmarks.ReferenceWork' \
+ --job "$BENCHMARK_JOB" \
+ --artifacts "$GITHUB_WORKSPACE/$RUNS/baseline"
+ ns=$(dotnet run scripts/benchmark-history.cs -- baseline --results "$RUNS/baseline")
+ echo "Reference workload: $ns ns"
+ echo "ns=$ns" >> "$GITHUB_OUTPUT"
+
+ - name: Measure the released version
+ if: github.event_name == 'release'
+ shell: bash
+ env:
+ TAG: ${{ github.event.release.tag_name }}
+ run: |
+ set -euo pipefail
+ version="${TAG#v}"
+ work="${RUNNER_TEMP}/bench-$version"
+ git worktree add --detach "$work" "$TAG"
+
+ (cd "$work" && dotnet run -c Release --project Semantics.Benchmarks -- \
+ --filter $HEADLINE_FILTER \
+ --job "$BENCHMARK_JOB" \
+ --artifacts "$GITHUB_WORKSPACE/$RUNS/$version")
+
+ dotnet run scripts/benchmark-history.cs -- ingest \
+ --history "$HISTORY" \
+ --results "$RUNS/$version" \
+ --version "$version" \
+ --commit "$(git rev-parse --short "$TAG^{commit}")" \
+ --date "$(git log -1 --format=%cs "$TAG")" \
+ --run-id "${{ github.run_id }}" \
+ --baseline-ns "${{ steps.baseline.outputs.ns }}"
+
+ git worktree remove --force "$work"
+
+ - name: Measure each backfill version
+ if: github.event_name == 'workflow_dispatch'
+ shell: bash
+ env:
+ VERSIONS: ${{ inputs.versions }}
+ BASELINE_NS: ${{ steps.baseline.outputs.ns }}
+ run: |
+ set -euo pipefail
+ read -ra versions <<< "$VERSIONS"
+
+ for version in "${versions[@]}"; do
+ echo "::group::$version"
+ # Through the environment rather than a -p: switch, because BenchmarkDotNet generates
+ # and builds a project of its own per run, which a property passed on the command line
+ # does not reach. MSBuild reads environment variables as properties in every project.
+ #
+ # A version whose API the current benchmarks cannot express is reported and skipped,
+ # rather than failing the whole backfill after the ones before it have been measured.
+ if ! BenchmarkAgainstVersion="$version" dotnet run -c Release --project Semantics.Benchmarks -- \
+ --filter $HEADLINE_FILTER \
+ --job "$BENCHMARK_JOB" \
+ --artifacts "$GITHUB_WORKSPACE/$RUNS/$version"; then
+ echo "::warning::$version could not be benchmarked by the current suite; skipping"
+ echo "::endgroup::"
+ continue
+ fi
+
+ tag="v$version"
+ commit=""
+ date=""
+ if git rev-parse -q --verify "$tag^{commit}" >/dev/null; then
+ commit="$(git rev-parse --short "$tag^{commit}")"
+ date="$(git log -1 --format=%cs "$tag")"
+ fi
+
+ # Skipped here too, and for the same reason: a package can build against these
+ # benchmarks and still throw from every one of them at run time, which BenchmarkDotNet
+ # reports as a table of NA rather than as a failure. Ingest refuses such a run, and
+ # the backfill carries on to the next version.
+ if ! dotnet run scripts/benchmark-history.cs -- ingest \
+ --history "$HISTORY" \
+ --results "$RUNS/$version" \
+ --version "$version" \
+ --commit "$commit" \
+ --date "$date" \
+ --run-id "${{ github.run_id }}" \
+ --baseline-ns "$BASELINE_NS"; then
+ echo "::warning::$version produced no usable measurement; skipping"
+ fi
+ echo "::endgroup::"
+ done
+
+ - name: Redraw the chart
+ shell: bash
+ run: dotnet run scripts/benchmark-history.cs -- render --history "$HISTORY" --out "$CHART"
+
+ - name: Commit the history and the chart
+ shell: bash
+ run: |
+ set -euo pipefail
+ git config user.name "github-actions[bot]"
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
+ # Staged first, then compared against the index: on the first run these files are new,
+ # and `git diff` alone does not see an untracked file, so the run would push nothing and
+ # still report success.
+ git add "$HISTORY" "${CHART%.svg}"*.svg
+ if git diff --cached --quiet; then
+ echo "Nothing changed."
+ exit 0
+ fi
+ # [skip ci] so that committing results does not start the pipeline over again.
+ git commit -m "[bot][skip ci] Update benchmark history"
+ branch="${{ github.event.repository.default_branch }}"
+ git pull --rebase origin "$branch"
+ git push origin "HEAD:$branch"
+
+ - name: Upload the raw reports
+ if: always()
+ uses: actions/upload-artifact@v7
+ with:
+ name: benchmark-history-${{ github.run_id }}
+ path: ${{ env.RUNS }}/
+ retention-days: 30
+ if-no-files-found: warn
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 732ea1e..42b6a60 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -11,6 +11,14 @@
same way a consumer is expected to. Not referenced by any shipping project. -->
+
+
+
+
+
diff --git a/README.md b/README.md
index 597b074..31ee730 100644
--- a/README.md
+++ b/README.md
@@ -160,6 +160,19 @@ public class UserService(ISemanticStringFactory emails)
}
```
+## Performance
+
+
+
+
+
+
+Every release measures a fixed set of benchmarks and adds a point to the chart; the numbers behind it are in [`docs/benchmarks/history.json`](docs/benchmarks/history.json), and the suite is [`Semantics.Benchmarks`](Semantics.Benchmarks/README.md).
+
+The grid is one operation per storage type rather than every operation at one storage type. A quantity is a `readonly record struct` over its `T` and does almost nothing of its own — a value is held in the SI base unit, so an operator is the storage type's arithmetic and a struct initialiser — so the same line of user code costs different things depending on the `T` it was written against, and a release changes it per `T`.
+
+Read the two halves differently. **Allocation is exact** — the same code allocates the same bytes on any machine, so a step in the top row is always a real change. **Time is measured on shared CI runners**, where the host a job happens to land on varies more than most releases do, so each time is divided by a reference workload measured in the same job. That cancels most of the difference between machines; what is left is indicative rather than precise.
+
## Architecture
The quantity system is metadata-driven. The single source of truth is `Semantics.SourceGenerators/Metadata/dimensions.json` (with `units.json`, `magnitudes.json`, `conversions.json`, `domains.json`, and `logarithmic.json` alongside it), and a Roslyn incremental generator emits the quantity records, unit-conversion factories, cross-dimensional operators, and physical constants. Generated output is committed to `Semantics.Quantities/Generated/` so the project compiles without first running the generator.
diff --git a/Semantics.Benchmarks/AssemblyInfo.cs b/Semantics.Benchmarks/AssemblyInfo.cs
new file mode 100644
index 0000000..7b1596d
--- /dev/null
+++ b/Semantics.Benchmarks/AssemblyInfo.cs
@@ -0,0 +1,3 @@
+// Copyright (c) 2023-2026 ktsu-dev contributors
+
+[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ktsu.Semantics.Test")]
diff --git a/Semantics.Benchmarks/BaselineBenchmarks.cs b/Semantics.Benchmarks/BaselineBenchmarks.cs
new file mode 100644
index 0000000..b205a68
--- /dev/null
+++ b/Semantics.Benchmarks/BaselineBenchmarks.cs
@@ -0,0 +1,59 @@
+// Copyright (c) 2023-2026 ktsu-dev contributors
+
+namespace ktsu.Semantics.Benchmarks;
+
+using BenchmarkDotNet.Attributes;
+
+///
+/// Measures a fixed workload that touches none of this library, so that timings taken on
+/// different machines can be compared.
+///
+///
+/// Every release is benchmarked in its own CI job, and a job lands on whichever shared runner is
+/// free — an x86-64-v3 or v4 host, at whatever clock its neighbours leave it. That difference is
+/// routinely larger than the changes a release makes, so a chart of raw times across releases
+/// mostly plots the runner.
+///
+/// This benchmark is the fixed point that makes the rest comparable. It is integer arithmetic over
+/// a value the JIT cannot fold away, chosen because it has no allocation, no library code, and no
+/// dependence on anything that changes between versions — so its measured time is a reading of the
+/// machine and nothing else. Dividing a benchmark's time by this one's, taken in the same job,
+/// cancels most of the difference between hosts. `scripts/benchmark-history.cs` records it on
+/// every entry and plots the ratio rather than the nanoseconds.
+///
+///
+/// It follows that this method's body must never change. Editing it silently rescales every
+/// comparison drawn against history recorded before the edit.
+///
+///
+[MemoryDiagnoser]
+public class BaselineBenchmarks
+{
+ // Read from a field rather than written as a literal, so that the loop cannot be constant
+ // folded into its own answer at JIT time.
+ private ulong seed;
+
+ ///
+ /// Sets the starting value.
+ ///
+ [GlobalSetup]
+ public void Setup() => seed = 0xcbf29ce484222325;
+
+ ///
+ /// Mixes a counter with a multiply-xor-shift step, the way a non-cryptographic hash does.
+ ///
+ /// The accumulated value, returned so that nothing here is dead code.
+ [Benchmark]
+ public ulong ReferenceWork()
+ {
+ ulong accumulator = seed;
+
+ for (int i = 0; i < 256; i++)
+ {
+ accumulator = (accumulator ^ (ulong)i) * 0x100000001b3;
+ accumulator ^= accumulator >> 29;
+ }
+
+ return accumulator;
+ }
+}
diff --git a/Semantics.Benchmarks/BenchmarkConfig.cs b/Semantics.Benchmarks/BenchmarkConfig.cs
new file mode 100644
index 0000000..f5b8164
--- /dev/null
+++ b/Semantics.Benchmarks/BenchmarkConfig.cs
@@ -0,0 +1,32 @@
+// Copyright (c) 2023-2026 ktsu-dev contributors
+
+namespace ktsu.Semantics.Benchmarks;
+
+using BenchmarkDotNet.Columns;
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Diagnosers;
+using BenchmarkDotNet.Exporters.Json;
+using BenchmarkDotNet.Order;
+
+///
+/// The configuration every benchmark in this assembly runs under.
+///
+internal static class BenchmarkConfig
+{
+ ///
+ /// Builds the configuration.
+ ///
+ /// The configuration to run benchmarks with.
+ ///
+ /// Allocation is reported alongside time because a quantity is a value type over a storage
+ /// type, so what allocates is the storage type rather than the quantity, and a change that
+ /// moves work between the two should be visible in the same table. Results are kept in
+ /// declaration order so that a summary reads the way the source does.
+ ///
+ internal static IConfig Create() =>
+ ManualConfig.Create(DefaultConfig.Instance)
+ .AddDiagnoser(MemoryDiagnoser.Default)
+ .AddColumn(RankColumn.Arabic)
+ .AddExporter(JsonExporter.Full)
+ .WithOrderer(new DefaultOrderer(SummaryOrderPolicy.Declared));
+}
diff --git a/Semantics.Benchmarks/ComparisonBenchmarks.cs b/Semantics.Benchmarks/ComparisonBenchmarks.cs
new file mode 100644
index 0000000..5c079ee
--- /dev/null
+++ b/Semantics.Benchmarks/ComparisonBenchmarks.cs
@@ -0,0 +1,68 @@
+// Copyright (c) 2023-2026 ktsu-dev contributors
+
+namespace ktsu.Semantics.Benchmarks;
+
+using System.Numerics;
+
+using BenchmarkDotNet.Attributes;
+
+using ktsu.PreciseNumber;
+using ktsu.Semantics.Quantities;
+
+///
+/// Measures comparing and equating quantities, by each of the two routes available.
+///
+///
+/// A quantity carries two comparisons, and they do not cost the same. The operators are the
+/// storage type's own, on a struct, and should allocate nothing.
+/// and go through PhysicalQuantityCore, which takes an
+/// IPhysicalQuantity<T> — an interface, so the argument is boxed at the call, and the
+/// dimensions are checked before the values are. That is a deliberate difference rather than an
+/// oversight: the interface route is the one that can refuse to compare a length with a mass, and
+/// this class is what says what it costs.
+///
+/// The storage type.
+[MemoryDiagnoser]
+[GenericTypeArguments(typeof(double))]
+[GenericTypeArguments(typeof(float))]
+[GenericTypeArguments(typeof(decimal))]
+[GenericTypeArguments(typeof(PreciseNumber))]
+public class ComparisonBenchmarks
+ where T : struct, INumber
+{
+ // 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.
+ private Length left = default!;
+ private Length right = default!;
+
+ ///
+ /// Prepares the operands.
+ ///
+ [GlobalSetup]
+ public void Setup()
+ {
+ left = Length.Create(Operands.Of("1234.5678901234567890123456"));
+ right = Length.Create(Operands.Of("1234.5678901234567890123457"));
+ }
+
+ /// Compares with the less-than operator.
+ /// Whether the left sorts before the right.
+ [Benchmark]
+ public bool LessThan() => left < right;
+
+ /// Compares through the dimension-checking interface.
+ /// The comparison.
+ [Benchmark]
+ public int CompareToInterface() => left.CompareTo(right);
+
+ /// Equates through the record's generated equality.
+ /// Whether the two are equal.
+ [Benchmark]
+ public bool EqualsRecord() => left.Equals(right);
+
+ /// Equates through the dimension-checking interface.
+ /// Whether the two are equal.
+ [Benchmark]
+ public bool EqualsInterface() => left.Equals((IPhysicalQuantity)right);
+}
diff --git a/Semantics.Benchmarks/ConstructionBenchmarks.cs b/Semantics.Benchmarks/ConstructionBenchmarks.cs
new file mode 100644
index 0000000..5c1b654
--- /dev/null
+++ b/Semantics.Benchmarks/ConstructionBenchmarks.cs
@@ -0,0 +1,67 @@
+// Copyright (c) 2023-2026 ktsu-dev contributors
+
+namespace ktsu.Semantics.Benchmarks;
+
+using System.Numerics;
+
+using BenchmarkDotNet.Attributes;
+
+using ktsu.PreciseNumber;
+using ktsu.Semantics.Quantities;
+
+///
+/// Measures building a quantity, through each of the three routes a unit factory can take.
+///
+///
+///
+/// is the floor: a struct initialiser and nothing else. Every other route
+/// here is that plus the work a unit costs, so the difference between them is the measurement.
+///
+///
+/// adds only the magnitude guard, because a metre already is the SI base
+/// unit. multiplies by a metric magnitude, and
+/// by a conversion constant; both read a Values<T>
+/// property that materialises the factor into the storage type once per closed generic and holds
+/// it. That holder is what 5.2.0 added, and it is why the storage-type axis is the interesting one
+/// here: for the factor was already a and nothing
+/// changed, while for it went from a 15-digit conversion of a double to the
+/// literal parsed at the type's own precision.
+///
+///
+/// The storage type.
+[MemoryDiagnoser]
+[GenericTypeArguments(typeof(double))]
+[GenericTypeArguments(typeof(float))]
+[GenericTypeArguments(typeof(decimal))]
+[GenericTypeArguments(typeof(PreciseNumber))]
+public class ConstructionBenchmarks
+ where T : struct, INumber
+{
+ private T value;
+
+ ///
+ /// Prepares the operand.
+ ///
+ [GlobalSetup]
+ public void Setup() => value = Operands.Of("1234.5678901234567890123456");
+
+ /// Builds a quantity from a value already in the SI base unit.
+ /// The quantity.
+ [Benchmark]
+ public Length Create() => Length.Create(value);
+
+ /// Builds a quantity through the base unit's factory, which is the guard alone.
+ /// The quantity.
+ [Benchmark]
+ public Length FromMeter() => Length.FromMeter(value);
+
+ /// Builds a quantity through a metric magnitude.
+ /// The quantity.
+ [Benchmark]
+ public Length FromKilometer() => Length.FromKilometer(value);
+
+ /// Builds a quantity through a conversion constant that is not a power of ten.
+ /// The quantity.
+ [Benchmark]
+ public Length FromNauticalMile() => Length.FromNauticalMile(value);
+}
diff --git a/Semantics.Benchmarks/Operands.cs b/Semantics.Benchmarks/Operands.cs
new file mode 100644
index 0000000..a2ba3e1
--- /dev/null
+++ b/Semantics.Benchmarks/Operands.cs
@@ -0,0 +1,28 @@
+// Copyright (c) 2023-2026 ktsu-dev contributors
+
+namespace ktsu.Semantics.Benchmarks;
+
+using System.Globalization;
+using System.Numerics;
+
+///
+/// Builds the storage-type values the benchmarks run against.
+///
+///
+/// Every value is parsed from text rather than converted from a , so that a
+/// storage type wide enough to hold more than a double's 15 digits actually receives them. A
+/// or a PreciseNumber seeded through a double would be measured
+/// carrying a double's worth of digits, which is the opposite of why those types are here.
+///
+internal static class Operands
+{
+ ///
+ /// Parses a value into the storage type.
+ ///
+ /// The storage type.
+ /// The decimal text to parse.
+ /// The parsed value.
+ internal static T Of(string text)
+ where T : struct, INumber =>
+ T.Parse(text, NumberStyles.Float, CultureInfo.InvariantCulture);
+}
diff --git a/Semantics.Benchmarks/OperatorBenchmarks.cs b/Semantics.Benchmarks/OperatorBenchmarks.cs
new file mode 100644
index 0000000..84eca61
--- /dev/null
+++ b/Semantics.Benchmarks/OperatorBenchmarks.cs
@@ -0,0 +1,95 @@
+// Copyright (c) 2023-2026 ktsu-dev contributors
+
+namespace ktsu.Semantics.Benchmarks;
+
+using System.Numerics;
+
+using BenchmarkDotNet.Attributes;
+
+using ktsu.PreciseNumber;
+using ktsu.Semantics.Quantities;
+
+///
+/// Measures the generated physics relationships.
+///
+///
+///
+/// Every value is held in its SI base unit, so a relationship operator is the storage type's own
+/// arithmetic and a struct initialiser — there is no conversion in the middle of an equation. That
+/// is the claim this class exists to keep honest: an operator should cost what the same arithmetic
+/// costs on bare , and is here as the comparison,
+/// being the one operation that changes no dimension.
+///
+///
+/// For and that claim comes back as a non-answer, and the
+/// non-answer is the point. The operands do not change between iterations, so a product of two of
+/// them is loop-invariant and the JIT hoists it clean out; BenchmarkDotNet then reports a
+/// ZeroMeasurement warning — the method is indistinguishable from an empty one. Nothing here can
+/// fix that without measuring the fix instead of the operator: an array of operands adds a load,
+/// a mutated field adds a store, and either would swamp the single instruction being asked about.
+/// Read those two rows as "below what the harness resolves", not as a number, and read the
+/// and PreciseNumber rows, which are far enough above the floor to
+/// mean something. The release chart draws none of these for the same reason.
+///
+///
+/// The storage type.
+[MemoryDiagnoser]
+[GenericTypeArguments(typeof(double))]
+[GenericTypeArguments(typeof(float))]
+[GenericTypeArguments(typeof(decimal))]
+[GenericTypeArguments(typeof(PreciseNumber))]
+public class OperatorBenchmarks
+ where T : struct, INumber
+{
+ // 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.
+ private Length length = default!;
+ private Length otherLength = default!;
+ private Force1D force = default!;
+ private Duration duration = default!;
+ private Velocity3D velocity = default!;
+
+ ///
+ /// Prepares the operands.
+ ///
+ [GlobalSetup]
+ public void Setup()
+ {
+ length = Length.Create(Operands.Of("1234.5678901234567890123456"));
+ otherLength = Length.Create(Operands.Of("8765.4321098765432109876543"));
+ force = Force1D.Create(Operands.Of("-98.76543210987654321"));
+ duration = Duration.Create(Operands.Of("12.3456789012345678"));
+ velocity = new()
+ {
+ X = Operands.Of("3.14159265358979323846"),
+ Y = Operands.Of("-2.71828182845904523536"),
+ Z = Operands.Of("1.41421356237309504880"),
+ };
+ }
+
+ /// Adds two quantities of the same dimension.
+ /// The sum.
+ [Benchmark]
+ public Length Add() => length + otherLength;
+
+ /// Multiplies two lengths, which lands on a different dimension.
+ /// The area.
+ [Benchmark]
+ public Area LengthTimesLength() => length * otherLength;
+
+ /// Multiplies a signed scalar quantity by a duration.
+ /// The momentum.
+ [Benchmark]
+ public Momentum1D ForceTimesDuration() => force * duration;
+
+ /// Multiplies a three-component quantity by a duration, componentwise.
+ /// The displacement.
+ [Benchmark]
+ public Displacement3D VelocityTimesDuration() => velocity * duration;
+
+ /// Divides one quantity by another of the same dimension, giving a bare ratio.
+ /// The ratio.
+ [Benchmark]
+ public T LengthOverLength() => length / otherLength;
+}
diff --git a/Semantics.Benchmarks/Program.cs b/Semantics.Benchmarks/Program.cs
new file mode 100644
index 0000000..3cf93f1
--- /dev/null
+++ b/Semantics.Benchmarks/Program.cs
@@ -0,0 +1,18 @@
+// Copyright (c) 2023-2026 ktsu-dev contributors
+
+namespace ktsu.Semantics.Benchmarks;
+
+using BenchmarkDotNet.Running;
+
+///
+/// Entry point for the benchmark suite.
+///
+internal static class Program
+{
+ ///
+ /// Runs the benchmarks named on the command line, or prompts for a selection when none are.
+ ///
+ /// Command line arguments, forwarded to BenchmarkDotNet.
+ internal static void Main(string[] args) =>
+ _ = BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, BenchmarkConfig.Create());
+}
diff --git a/Semantics.Benchmarks/README.md b/Semantics.Benchmarks/README.md
new file mode 100644
index 0000000..1e8bc48
--- /dev/null
+++ b/Semantics.Benchmarks/README.md
@@ -0,0 +1,97 @@
+# Semantics Benchmarks
+
+A [BenchmarkDotNet](https://benchmarkdotnet.org) suite covering the quantity system: building a
+quantity from a unit, reading it back out in one, the generated physics operators, the
+componentwise vector operations, and comparison.
+
+## The axis that matters here is the storage type
+
+Every generated quantity is a `readonly record struct` over a storage type — `Length`,
+`Length`, `Length` — and it does almost nothing of its own. A value is held
+in the dimension's SI base unit, so an operator is the storage type's arithmetic and a struct
+initialiser, and a unit factory is that plus one multiplication. What a quantity costs is therefore
+mostly what its `T` costs, and a change to this library shows up differently per `T`.
+
+So every class here is generic and carries `[GenericTypeArguments]` for `double`, `float`,
+`decimal` and `PreciseNumber`, and the summary is read **across** those four rather than down one
+of them. The four are chosen to span the interesting ground: two binary floating point types the
+hardware does in a register, one decimal type the runtime does in software, and one arbitrary
+precision type from another package.
+
+Operands are **parsed from text**, never converted from a `double`. A `decimal` or a
+`PreciseNumber` seeded through a double would be measured carrying a double's worth of digits,
+which is the opposite of why those types are in the list.
+
+## Running
+
+From the repository root:
+
+```bash
+# Pick benchmarks from an interactive list
+dotnet run -c Release --project Semantics.Benchmarks
+
+# Run everything
+dotnet run -c Release --project Semantics.Benchmarks -- --filter '*'
+
+# One class across all four storage types, or one storage type across all classes
+dotnet run -c Release --project Semantics.Benchmarks -- --filter '*VectorBenchmarks*'
+dotnet run -c Release --project Semantics.Benchmarks -- --filter '**'
+```
+
+## Measuring a published release
+
+Set `BenchmarkAgainstVersion` and the suite measures that package instead of the working copy:
+
+```bash
+BenchmarkAgainstVersion=5.2.0 dotnet run -c Release --project Semantics.Benchmarks -- --filter '**'
+```
+
+Set it **in the environment, not with `-p:`**. BenchmarkDotNet generates and builds a project of
+its own for each run, and a property passed on the command line does not reach that project — it
+would build the benchmark assembly against the version you asked for and the harness against the
+one pinned centrally, which fails to compile if a type changed shape between them. MSBuild reads
+environment variables as properties in every project, so the environment form reaches both.
+
+This switch is how `docs/benchmarks/` is filled. No tag in this repository carries a benchmark
+project, so there is no older source to check out and run; and measuring packages is the better
+comparison anyway, because every version is timed by identical benchmark code rather than by
+whatever each tag happened to ship. A version whose API the current benchmarks cannot express is
+reported and skipped rather than failing the backfill — 4.0 made every quantity a record struct and
+5.0 removed four operators, so reaching back far enough eventually finds a version this suite
+cannot ask.
+
+It is also why nothing here touches an internal member: the `InternalsVisibleTo` that would expose
+one names the test assembly, and a benchmark built on internals could only ever measure the
+working copy.
+
+## What each class is for
+
+| Class | What it isolates |
+|---|---|
+| `ConstructionBenchmarks` | `Create` is the floor — a struct initialiser. `FromMeter` adds the magnitude guard alone. `FromKilometer` and `FromNauticalMile` add a factor read from a `Values` holder, which is what 5.2.0 introduced and what gives a `decimal` quantity its full precision. |
+| `UnitConversionBenchmarks` | The way back out, through `In(unit)`. `InCelsius` is the one affine case, carrying an offset as well as a factor. |
+| `OperatorBenchmarks` | The generated physics relationships. `Add` is the control, being the one operation that changes no dimension. |
+| `VectorBenchmarks` | Componentwise arithmetic, and the square root two of the operations need. `LengthSquared` is `Length` without the root, so the gap between them is the root alone — which for `decimal` and `PreciseNumber` is a Newton refinement rather than a hardware instruction. |
+| `ComparisonBenchmarks` | The two comparison routes. The operators are the storage type's own; `CompareTo` and the `IPhysicalQuantity` overload of `Equals` box the argument and check dimensions first, which is what buys the ability to refuse a length against a mass. |
+| `BaselineBenchmarks` | Touches none of this library. It exists so that timings taken in different CI jobs can be compared; see its remarks, and do not edit its body. |
+
+### An operator on a `double` is below the floor
+
+`OperatorBenchmarks` over `double` and `float` comes back with a ZeroMeasurement warning: the
+method is indistinguishable from an empty one. That is not a broken benchmark, it is the answer.
+A relationship operator on a binary floating point type is one machine instruction over operands
+the loop does not change, so the JIT hoists it out entirely.
+
+It cannot be fixed without measuring the fix instead of the operator — an array of operands adds a
+load, a mutated field adds a store, and either would swamp the instruction being asked about. So
+those rows are read as "below what the harness resolves" rather than as numbers, the `decimal` and
+`PreciseNumber` rows in the same table are the ones that mean something, and the release chart
+draws no operator panel at all.
+
+## Reading the results
+
+Allocation is reported alongside time and matters just as much. A quantity is a value type, so
+anything allocated was allocated by the storage type or by boxing — a `double` or `decimal`
+quantity should show 0 B for arithmetic and comparison, a `PreciseNumber` one should show its
+`BigInteger` digit arrays, and the interface comparison routes should show a box on every storage
+type.
diff --git a/Semantics.Benchmarks/Semantics.Benchmarks.csproj b/Semantics.Benchmarks/Semantics.Benchmarks.csproj
new file mode 100644
index 0000000..d76a90e
--- /dev/null
+++ b/Semantics.Benchmarks/Semantics.Benchmarks.csproj
@@ -0,0 +1,50 @@
+
+
+
+
+
+ Exe
+ net10.0
+
+
+ Semantics.Benchmarks
+ ktsu.Semantics.Benchmarks
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Semantics.Benchmarks/UnitConversionBenchmarks.cs b/Semantics.Benchmarks/UnitConversionBenchmarks.cs
new file mode 100644
index 0000000..eb2b7ee
--- /dev/null
+++ b/Semantics.Benchmarks/UnitConversionBenchmarks.cs
@@ -0,0 +1,78 @@
+// Copyright (c) 2023-2026 ktsu-dev contributors
+
+namespace ktsu.Semantics.Benchmarks;
+
+using System.Numerics;
+
+using BenchmarkDotNet.Attributes;
+
+using ktsu.PreciseNumber;
+using ktsu.Semantics.Quantities;
+using ktsu.Semantics.Quantities.Units;
+
+///
+/// Measures reading a quantity back out in a unit.
+///
+///
+///
+/// This is the other half of the boundary measures: a
+/// value goes in through a factory and comes back out through In(unit), and between the two
+/// it is held in the SI base unit. Anything that uses the library at all crosses this pair, which
+/// is why both are measured rather than only the arithmetic between them.
+///
+///
+/// is the affine case. Every other conversion here is a multiplication;
+/// a temperature also carries an offset, so it reads ToBaseOffsetAs as well and is the one
+/// unit family where the second half of the affine conversion is not free.
+///
+///
+/// The storage type.
+[MemoryDiagnoser]
+[GenericTypeArguments(typeof(double))]
+[GenericTypeArguments(typeof(float))]
+[GenericTypeArguments(typeof(decimal))]
+[GenericTypeArguments(typeof(PreciseNumber))]
+public class UnitConversionBenchmarks
+ where T : struct, INumber
+{
+ private static readonly Meter Meter = new();
+ private static readonly Kilometer Kilometer = new();
+ private static readonly NauticalMile NauticalMile = new();
+ private static readonly Celsius Celsius = new();
+
+ // 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.
+ private Length length = default!;
+ private Temperature temperature = default!;
+
+ ///
+ /// Prepares the operands.
+ ///
+ [GlobalSetup]
+ public void Setup()
+ {
+ length = Length.Create(Operands.Of("1234.5678901234567890123456"));
+ temperature = Temperature.Create(Operands.Of("293.15"));
+ }
+
+ /// Reads the value back in the base unit, where the factor is one.
+ /// The value in metres.
+ [Benchmark]
+ public T InMeter() => length.In(Meter);
+
+ /// Reads the value back through a metric magnitude.
+ /// The value in kilometres.
+ [Benchmark]
+ public T InKilometer() => length.In(Kilometer);
+
+ /// Reads the value back through a conversion constant.
+ /// The value in nautical miles.
+ [Benchmark]
+ public T InNauticalMile() => length.In(NauticalMile);
+
+ /// Reads a temperature back through a conversion carrying an offset.
+ /// The value in degrees Celsius.
+ [Benchmark]
+ public T InCelsius() => temperature.In(Celsius);
+}
diff --git a/Semantics.Benchmarks/VectorBenchmarks.cs b/Semantics.Benchmarks/VectorBenchmarks.cs
new file mode 100644
index 0000000..7ff35fb
--- /dev/null
+++ b/Semantics.Benchmarks/VectorBenchmarks.cs
@@ -0,0 +1,88 @@
+// Copyright (c) 2023-2026 ktsu-dev contributors
+
+namespace ktsu.Semantics.Benchmarks;
+
+using System.Numerics;
+
+using BenchmarkDotNet.Attributes;
+
+using ktsu.PreciseNumber;
+using ktsu.Semantics.Quantities;
+
+///
+/// Measures the componentwise vector operations, and the square root two of them need.
+///
+///
+///
+/// and are written-out arithmetic and nothing else, so they
+/// are the control. and are the same arithmetic plus
+/// StorageMath.Sqrt, and that is the whole point of the class: a binary floating point type
+/// takes the Math.Sqrt round trip, and anything else is seeded from that root and refined
+/// with Newton steps in its own arithmetic until it settles. The gap between the two rows is the
+/// cost of those steps, and it is a per-storage-type answer rather than a single number.
+///
+///
+/// is here to separate the two halves: it is
+/// without the root, so the difference between them is the root alone.
+///
+///
+/// The storage type.
+[MemoryDiagnoser]
+[GenericTypeArguments(typeof(double))]
+[GenericTypeArguments(typeof(float))]
+[GenericTypeArguments(typeof(decimal))]
+[GenericTypeArguments(typeof(PreciseNumber))]
+public class VectorBenchmarks
+ where T : struct, INumber
+{
+ // 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.
+ private Displacement3D left = default!;
+ private Displacement3D right = default!;
+
+ ///
+ /// Prepares the operands.
+ ///
+ [GlobalSetup]
+ public void Setup()
+ {
+ left = new()
+ {
+ X = Operands.Of("3.14159265358979323846"),
+ Y = Operands.Of("-2.71828182845904523536"),
+ Z = Operands.Of("1.41421356237309504880"),
+ };
+ right = new()
+ {
+ X = Operands.Of("1.61803398874989484820"),
+ Y = Operands.Of("0.57721566490153286060"),
+ Z = Operands.Of("-1.73205080756887729352"),
+ };
+ }
+
+ /// Sums the squares of the components, without taking a root.
+ /// The squared length.
+ [Benchmark]
+ public T LengthSquared() => left.LengthSquared();
+
+ /// Takes the length, which is the squared length and a root.
+ /// The length.
+ [Benchmark]
+ public T Length() => left.Length();
+
+ /// Takes the distance between two vectors, which is a difference and a root.
+ /// The distance.
+ [Benchmark]
+ public T Distance() => left.Distance(right);
+
+ /// Takes the dot product, which is written-out arithmetic alone.
+ /// The dot product.
+ [Benchmark]
+ public T Dot() => left.Dot(right);
+
+ /// Takes the cross product, which is written-out arithmetic alone.
+ /// The cross product.
+ [Benchmark]
+ public Displacement3D Cross() => left.Cross(right);
+}
diff --git a/Semantics.sln b/Semantics.sln
index b51638b..1742535 100644
--- a/Semantics.sln
+++ b/Semantics.sln
@@ -31,6 +31,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Semantics.Cpp.Test", "Seman
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Semantics.Quantities.Precise", "Semantics.Quantities.Precise\Semantics.Quantities.Precise.csproj", "{5A67DFC3-E2D2-41B9-9E42-0B4727AF0C7D}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Semantics.Benchmarks", "Semantics.Benchmarks\Semantics.Benchmarks.csproj", "{7F2A4C61-8D35-4B90-A6E2-1C0F5D3B8E47}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -209,6 +211,10 @@ Global
{5A67DFC3-E2D2-41B9-9E42-0B4727AF0C7D}.Release|x64.Build.0 = Release|Any CPU
{5A67DFC3-E2D2-41B9-9E42-0B4727AF0C7D}.Release|x86.ActiveCfg = Release|Any CPU
{5A67DFC3-E2D2-41B9-9E42-0B4727AF0C7D}.Release|x86.Build.0 = Release|Any CPU
+ {7F2A4C61-8D35-4B90-A6E2-1C0F5D3B8E47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7F2A4C61-8D35-4B90-A6E2-1C0F5D3B8E47}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7F2A4C61-8D35-4B90-A6E2-1C0F5D3B8E47}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7F2A4C61-8D35-4B90-A6E2-1C0F5D3B8E47}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/docs/benchmarks/history.json b/docs/benchmarks/history.json
new file mode 100644
index 0000000..ec8ff53
--- /dev/null
+++ b/docs/benchmarks/history.json
@@ -0,0 +1,1315 @@
+{
+ "schemaVersion": 1,
+ "entries": [
+ {
+ "version": "3.3.1",
+ "commit": "fffa086",
+ "date": "2026-09-09",
+ "cpu": "Intel Xeon Processor 2.80GHz",
+ "runtime": ".NET 10.0.12 (10.0.12, 10.0.1226.42308)",
+ "baselineNs": 454.7423,
+ "runId": "local-seed",
+ "benchmarks": {
+ "ComparisonBenchmarks\u003CDecimal\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 8.6608,
+ "allocatedBytes": 0
+ }
+ },
+ "ComparisonBenchmarks\u003CDouble\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 4.7739,
+ "allocatedBytes": 0
+ }
+ },
+ "ComparisonBenchmarks\u003CPreciseNumber\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 11.5986,
+ "allocatedBytes": 0
+ }
+ },
+ "ComparisonBenchmarks\u003CSingle\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 5.6374,
+ "allocatedBytes": 0
+ }
+ },
+ "ConstructionBenchmarks\u003CDecimal\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 43.4281,
+ "allocatedBytes": 64
+ }
+ },
+ "ConstructionBenchmarks\u003CDouble\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 27.3741,
+ "allocatedBytes": 48
+ }
+ },
+ "ConstructionBenchmarks\u003CPreciseNumber\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 519.2756,
+ "allocatedBytes": 120
+ }
+ },
+ "ConstructionBenchmarks\u003CSingle\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 28.3839,
+ "allocatedBytes": 48
+ }
+ },
+ "OperatorBenchmarks\u003CDecimal\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 81.1407,
+ "allocatedBytes": 64
+ }
+ },
+ "OperatorBenchmarks\u003CDouble\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 35.0934,
+ "allocatedBytes": 48
+ }
+ },
+ "OperatorBenchmarks\u003CPreciseNumber\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 231.1584,
+ "allocatedBytes": 128
+ }
+ },
+ "OperatorBenchmarks\u003CSingle\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 28.2983,
+ "allocatedBytes": 48
+ }
+ },
+ "UnitConversionBenchmarks\u003CDecimal\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 71.2753,
+ "allocatedBytes": 0
+ }
+ },
+ "UnitConversionBenchmarks\u003CDouble\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 7.3582,
+ "allocatedBytes": 0
+ }
+ },
+ "UnitConversionBenchmarks\u003CPreciseNumber\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 989.1301,
+ "allocatedBytes": 232
+ }
+ },
+ "UnitConversionBenchmarks\u003CSingle\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 7.1397,
+ "allocatedBytes": 0
+ }
+ },
+ "VectorBenchmarks\u003CDecimal\u003E.Length": {
+ "": {
+ "meanNs": 163.2781,
+ "allocatedBytes": 0
+ }
+ },
+ "VectorBenchmarks\u003CDouble\u003E.Length": {
+ "": {
+ "meanNs": 1.002,
+ "allocatedBytes": 0
+ }
+ },
+ "VectorBenchmarks\u003CPreciseNumber\u003E.Length": {
+ "": {
+ "meanNs": 1827.4723,
+ "allocatedBytes": 312
+ }
+ },
+ "VectorBenchmarks\u003CSingle\u003E.Length": {
+ "": {
+ "meanNs": 0.8253,
+ "allocatedBytes": 0
+ }
+ }
+ }
+ },
+ {
+ "version": "4.0.0",
+ "commit": "496d895",
+ "date": "2026-09-10",
+ "cpu": "Intel Xeon Processor 2.80GHz",
+ "runtime": ".NET 10.0.12 (10.0.12, 10.0.1226.42308)",
+ "baselineNs": 454.7423,
+ "runId": "local-seed",
+ "benchmarks": {
+ "ComparisonBenchmarks\u003CDecimal\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 20.3958,
+ "allocatedBytes": 32
+ }
+ },
+ "ComparisonBenchmarks\u003CDouble\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 14.2212,
+ "allocatedBytes": 24
+ }
+ },
+ "ComparisonBenchmarks\u003CPreciseNumber\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 34.4273,
+ "allocatedBytes": 40
+ }
+ },
+ "ComparisonBenchmarks\u003CSingle\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 13.928,
+ "allocatedBytes": 24
+ }
+ },
+ "ConstructionBenchmarks\u003CDecimal\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 9.117,
+ "allocatedBytes": 0
+ }
+ },
+ "ConstructionBenchmarks\u003CDouble\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 3.364,
+ "allocatedBytes": 0
+ }
+ },
+ "ConstructionBenchmarks\u003CPreciseNumber\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 443.2553,
+ "allocatedBytes": 40
+ }
+ },
+ "ConstructionBenchmarks\u003CSingle\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 0.9414,
+ "allocatedBytes": 0
+ }
+ },
+ "OperatorBenchmarks\u003CDecimal\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 49.7154,
+ "allocatedBytes": 0
+ }
+ },
+ "OperatorBenchmarks\u003CDouble\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 0.0007,
+ "allocatedBytes": 0
+ }
+ },
+ "OperatorBenchmarks\u003CPreciseNumber\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 169.1601,
+ "allocatedBytes": 48
+ }
+ },
+ "OperatorBenchmarks\u003CSingle\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 0,
+ "allocatedBytes": 0
+ }
+ },
+ "UnitConversionBenchmarks\u003CDecimal\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 70.1256,
+ "allocatedBytes": 0
+ }
+ },
+ "UnitConversionBenchmarks\u003CDouble\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 8.4243,
+ "allocatedBytes": 0
+ }
+ },
+ "UnitConversionBenchmarks\u003CPreciseNumber\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 1045.9718,
+ "allocatedBytes": 232
+ }
+ },
+ "UnitConversionBenchmarks\u003CSingle\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 7.1489,
+ "allocatedBytes": 0
+ }
+ },
+ "VectorBenchmarks\u003CDecimal\u003E.Length": {
+ "": {
+ "meanNs": 162.9532,
+ "allocatedBytes": 0
+ }
+ },
+ "VectorBenchmarks\u003CDouble\u003E.Length": {
+ "": {
+ "meanNs": 0.2999,
+ "allocatedBytes": 0
+ }
+ },
+ "VectorBenchmarks\u003CPreciseNumber\u003E.Length": {
+ "": {
+ "meanNs": 1791.7621,
+ "allocatedBytes": 312
+ }
+ },
+ "VectorBenchmarks\u003CSingle\u003E.Length": {
+ "": {
+ "meanNs": 0.7062,
+ "allocatedBytes": 0
+ }
+ }
+ }
+ },
+ {
+ "version": "4.1.0",
+ "commit": "fa79094",
+ "date": "2026-09-11",
+ "cpu": "Intel Xeon Processor 2.80GHz",
+ "runtime": ".NET 10.0.12 (10.0.12, 10.0.1226.42308)",
+ "baselineNs": 454.7423,
+ "runId": "local-seed",
+ "benchmarks": {
+ "ComparisonBenchmarks\u003CDecimal\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 19.4354,
+ "allocatedBytes": 32
+ }
+ },
+ "ComparisonBenchmarks\u003CDouble\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 14.2071,
+ "allocatedBytes": 24
+ }
+ },
+ "ComparisonBenchmarks\u003CPreciseNumber\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 32.335,
+ "allocatedBytes": 40
+ }
+ },
+ "ComparisonBenchmarks\u003CSingle\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 13.1068,
+ "allocatedBytes": 24
+ }
+ },
+ "ConstructionBenchmarks\u003CDecimal\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 9.0262,
+ "allocatedBytes": 0
+ }
+ },
+ "ConstructionBenchmarks\u003CDouble\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 3.3646,
+ "allocatedBytes": 0
+ }
+ },
+ "ConstructionBenchmarks\u003CPreciseNumber\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 458.7913,
+ "allocatedBytes": 40
+ }
+ },
+ "ConstructionBenchmarks\u003CSingle\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 1.1227,
+ "allocatedBytes": 0
+ }
+ },
+ "OperatorBenchmarks\u003CDecimal\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 49.6151,
+ "allocatedBytes": 0
+ }
+ },
+ "OperatorBenchmarks\u003CDouble\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 0.021,
+ "allocatedBytes": 0
+ }
+ },
+ "OperatorBenchmarks\u003CPreciseNumber\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 169.2628,
+ "allocatedBytes": 48
+ }
+ },
+ "OperatorBenchmarks\u003CSingle\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 0.0038,
+ "allocatedBytes": 0
+ }
+ },
+ "UnitConversionBenchmarks\u003CDecimal\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 70.5234,
+ "allocatedBytes": 0
+ }
+ },
+ "UnitConversionBenchmarks\u003CDouble\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 7.5685,
+ "allocatedBytes": 0
+ }
+ },
+ "UnitConversionBenchmarks\u003CPreciseNumber\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 999.187,
+ "allocatedBytes": 232
+ }
+ },
+ "UnitConversionBenchmarks\u003CSingle\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 7.9879,
+ "allocatedBytes": 0
+ }
+ },
+ "VectorBenchmarks\u003CDecimal\u003E.Length": {
+ "": {
+ "meanNs": 165.9375,
+ "allocatedBytes": 0
+ }
+ },
+ "VectorBenchmarks\u003CDouble\u003E.Length": {
+ "": {
+ "meanNs": 0.3614,
+ "allocatedBytes": 0
+ }
+ },
+ "VectorBenchmarks\u003CPreciseNumber\u003E.Length": {
+ "": {
+ "meanNs": 1799.8658,
+ "allocatedBytes": 312
+ }
+ },
+ "VectorBenchmarks\u003CSingle\u003E.Length": {
+ "": {
+ "meanNs": 0.7097,
+ "allocatedBytes": 0
+ }
+ }
+ }
+ },
+ {
+ "version": "4.2.0",
+ "commit": "5e19d32",
+ "date": "2026-09-11",
+ "cpu": "Intel Xeon Processor 2.80GHz",
+ "runtime": ".NET 10.0.12 (10.0.12, 10.0.1226.42308)",
+ "baselineNs": 454.7423,
+ "runId": "local-seed",
+ "benchmarks": {
+ "ComparisonBenchmarks\u003CDecimal\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 19.3255,
+ "allocatedBytes": 32
+ }
+ },
+ "ComparisonBenchmarks\u003CDouble\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 13.9245,
+ "allocatedBytes": 24
+ }
+ },
+ "ComparisonBenchmarks\u003CPreciseNumber\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 34.1165,
+ "allocatedBytes": 40
+ }
+ },
+ "ComparisonBenchmarks\u003CSingle\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 13.375,
+ "allocatedBytes": 24
+ }
+ },
+ "ConstructionBenchmarks\u003CDecimal\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 9.3505,
+ "allocatedBytes": 0
+ }
+ },
+ "ConstructionBenchmarks\u003CDouble\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 3.3425,
+ "allocatedBytes": 0
+ }
+ },
+ "ConstructionBenchmarks\u003CPreciseNumber\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 444.181,
+ "allocatedBytes": 40
+ }
+ },
+ "ConstructionBenchmarks\u003CSingle\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 1.0971,
+ "allocatedBytes": 0
+ }
+ },
+ "OperatorBenchmarks\u003CDecimal\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 49.8869,
+ "allocatedBytes": 0
+ }
+ },
+ "OperatorBenchmarks\u003CDouble\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 0.5551,
+ "allocatedBytes": 0
+ }
+ },
+ "OperatorBenchmarks\u003CPreciseNumber\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 171.1742,
+ "allocatedBytes": 48
+ }
+ },
+ "OperatorBenchmarks\u003CSingle\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 0.0061,
+ "allocatedBytes": 0
+ }
+ },
+ "UnitConversionBenchmarks\u003CDecimal\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 73.8065,
+ "allocatedBytes": 0
+ }
+ },
+ "UnitConversionBenchmarks\u003CDouble\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 7.9266,
+ "allocatedBytes": 0
+ }
+ },
+ "UnitConversionBenchmarks\u003CPreciseNumber\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 1027.7254,
+ "allocatedBytes": 232
+ }
+ },
+ "UnitConversionBenchmarks\u003CSingle\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 7.0378,
+ "allocatedBytes": 0
+ }
+ },
+ "VectorBenchmarks\u003CDecimal\u003E.Length": {
+ "": {
+ "meanNs": 164.2486,
+ "allocatedBytes": 0
+ }
+ },
+ "VectorBenchmarks\u003CDouble\u003E.Length": {
+ "": {
+ "meanNs": 0.4013,
+ "allocatedBytes": 0
+ }
+ },
+ "VectorBenchmarks\u003CPreciseNumber\u003E.Length": {
+ "": {
+ "meanNs": 1808.3943,
+ "allocatedBytes": 312
+ }
+ },
+ "VectorBenchmarks\u003CSingle\u003E.Length": {
+ "": {
+ "meanNs": 0.772,
+ "allocatedBytes": 0
+ }
+ }
+ }
+ },
+ {
+ "version": "4.3.2",
+ "commit": "256def6",
+ "date": "2026-09-12",
+ "cpu": "Intel Xeon Processor 2.80GHz",
+ "runtime": ".NET 10.0.12 (10.0.12, 10.0.1226.42308)",
+ "baselineNs": 454.7423,
+ "runId": "local-seed",
+ "benchmarks": {
+ "ComparisonBenchmarks\u003CDecimal\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 19.7476,
+ "allocatedBytes": 32
+ }
+ },
+ "ComparisonBenchmarks\u003CDouble\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 14.1942,
+ "allocatedBytes": 24
+ }
+ },
+ "ComparisonBenchmarks\u003CPreciseNumber\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 31.7734,
+ "allocatedBytes": 40
+ }
+ },
+ "ComparisonBenchmarks\u003CSingle\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 14.6629,
+ "allocatedBytes": 24
+ }
+ },
+ "ConstructionBenchmarks\u003CDecimal\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 9.1464,
+ "allocatedBytes": 0
+ }
+ },
+ "ConstructionBenchmarks\u003CDouble\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 3.3107,
+ "allocatedBytes": 0
+ }
+ },
+ "ConstructionBenchmarks\u003CPreciseNumber\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 439.0509,
+ "allocatedBytes": 40
+ }
+ },
+ "ConstructionBenchmarks\u003CSingle\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 1.0845,
+ "allocatedBytes": 0
+ }
+ },
+ "OperatorBenchmarks\u003CDecimal\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 49.6864,
+ "allocatedBytes": 0
+ }
+ },
+ "OperatorBenchmarks\u003CDouble\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 0.0064,
+ "allocatedBytes": 0
+ }
+ },
+ "OperatorBenchmarks\u003CPreciseNumber\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 153.0474,
+ "allocatedBytes": 48
+ }
+ },
+ "OperatorBenchmarks\u003CSingle\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 0.0215,
+ "allocatedBytes": 0
+ }
+ },
+ "UnitConversionBenchmarks\u003CDecimal\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 73.5654,
+ "allocatedBytes": 0
+ }
+ },
+ "UnitConversionBenchmarks\u003CDouble\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 6.9238,
+ "allocatedBytes": 0
+ }
+ },
+ "UnitConversionBenchmarks\u003CPreciseNumber\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 999.566,
+ "allocatedBytes": 232
+ }
+ },
+ "UnitConversionBenchmarks\u003CSingle\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 7.4833,
+ "allocatedBytes": 0
+ }
+ },
+ "VectorBenchmarks\u003CDecimal\u003E.Length": {
+ "": {
+ "meanNs": 164.8533,
+ "allocatedBytes": 0
+ }
+ },
+ "VectorBenchmarks\u003CDouble\u003E.Length": {
+ "": {
+ "meanNs": 0.4858,
+ "allocatedBytes": 0
+ }
+ },
+ "VectorBenchmarks\u003CPreciseNumber\u003E.Length": {
+ "": {
+ "meanNs": 1791.8562,
+ "allocatedBytes": 312
+ }
+ },
+ "VectorBenchmarks\u003CSingle\u003E.Length": {
+ "": {
+ "meanNs": 0.83,
+ "allocatedBytes": 0
+ }
+ }
+ }
+ },
+ {
+ "version": "5.0.0",
+ "commit": "bb69de0",
+ "date": "2026-09-12",
+ "cpu": "Intel Xeon Processor 2.80GHz",
+ "runtime": ".NET 10.0.12 (10.0.12, 10.0.1226.42308)",
+ "baselineNs": 454.7423,
+ "runId": "local-seed",
+ "benchmarks": {
+ "ComparisonBenchmarks\u003CDecimal\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 17.0531,
+ "allocatedBytes": 32
+ }
+ },
+ "ComparisonBenchmarks\u003CDouble\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 14.1983,
+ "allocatedBytes": 24
+ }
+ },
+ "ComparisonBenchmarks\u003CPreciseNumber\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 27.5663,
+ "allocatedBytes": 40
+ }
+ },
+ "ComparisonBenchmarks\u003CSingle\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 12.3376,
+ "allocatedBytes": 24
+ }
+ },
+ "ConstructionBenchmarks\u003CDecimal\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 9.1429,
+ "allocatedBytes": 0
+ }
+ },
+ "ConstructionBenchmarks\u003CDouble\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 2.1024,
+ "allocatedBytes": 0
+ }
+ },
+ "ConstructionBenchmarks\u003CPreciseNumber\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 440.8756,
+ "allocatedBytes": 40
+ }
+ },
+ "ConstructionBenchmarks\u003CSingle\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 1.1355,
+ "allocatedBytes": 0
+ }
+ },
+ "OperatorBenchmarks\u003CDecimal\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 48.7206,
+ "allocatedBytes": 0
+ }
+ },
+ "OperatorBenchmarks\u003CDouble\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 0.001,
+ "allocatedBytes": 0
+ }
+ },
+ "OperatorBenchmarks\u003CPreciseNumber\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 167.6413,
+ "allocatedBytes": 48
+ }
+ },
+ "OperatorBenchmarks\u003CSingle\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 0.5091,
+ "allocatedBytes": 0
+ }
+ },
+ "UnitConversionBenchmarks\u003CDecimal\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 76.0165,
+ "allocatedBytes": 0
+ }
+ },
+ "UnitConversionBenchmarks\u003CDouble\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 6.8443,
+ "allocatedBytes": 0
+ }
+ },
+ "UnitConversionBenchmarks\u003CPreciseNumber\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 1014.094,
+ "allocatedBytes": 232
+ }
+ },
+ "UnitConversionBenchmarks\u003CSingle\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 7.7742,
+ "allocatedBytes": 0
+ }
+ },
+ "VectorBenchmarks\u003CDecimal\u003E.Length": {
+ "": {
+ "meanNs": 161.0405,
+ "allocatedBytes": 0
+ }
+ },
+ "VectorBenchmarks\u003CDouble\u003E.Length": {
+ "": {
+ "meanNs": 0.4173,
+ "allocatedBytes": 0
+ }
+ },
+ "VectorBenchmarks\u003CPreciseNumber\u003E.Length": {
+ "": {
+ "meanNs": 1778.1463,
+ "allocatedBytes": 312
+ }
+ },
+ "VectorBenchmarks\u003CSingle\u003E.Length": {
+ "": {
+ "meanNs": 0.6495,
+ "allocatedBytes": 0
+ }
+ }
+ }
+ },
+ {
+ "version": "5.1.0",
+ "commit": "4c71e14",
+ "date": "2026-09-13",
+ "cpu": "Intel Xeon Processor 2.80GHz",
+ "runtime": ".NET 10.0.12 (10.0.12, 10.0.1226.42308)",
+ "baselineNs": 454.7423,
+ "runId": "local-seed",
+ "benchmarks": {
+ "ComparisonBenchmarks\u003CDecimal\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 17.2642,
+ "allocatedBytes": 32
+ }
+ },
+ "ComparisonBenchmarks\u003CDouble\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 14.6504,
+ "allocatedBytes": 24
+ }
+ },
+ "ComparisonBenchmarks\u003CPreciseNumber\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 28.4428,
+ "allocatedBytes": 40
+ }
+ },
+ "ComparisonBenchmarks\u003CSingle\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 14.593,
+ "allocatedBytes": 24
+ }
+ },
+ "ConstructionBenchmarks\u003CDecimal\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 9.0442,
+ "allocatedBytes": 0
+ }
+ },
+ "ConstructionBenchmarks\u003CDouble\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 2.1058,
+ "allocatedBytes": 0
+ }
+ },
+ "ConstructionBenchmarks\u003CPreciseNumber\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 443.6043,
+ "allocatedBytes": 40
+ }
+ },
+ "ConstructionBenchmarks\u003CSingle\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 1.047,
+ "allocatedBytes": 0
+ }
+ },
+ "OperatorBenchmarks\u003CDecimal\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 49.4833,
+ "allocatedBytes": 0
+ }
+ },
+ "OperatorBenchmarks\u003CDouble\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 0.0023,
+ "allocatedBytes": 0
+ }
+ },
+ "OperatorBenchmarks\u003CPreciseNumber\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 168.0931,
+ "allocatedBytes": 48
+ }
+ },
+ "OperatorBenchmarks\u003CSingle\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 0.0017,
+ "allocatedBytes": 0
+ }
+ },
+ "UnitConversionBenchmarks\u003CDecimal\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 70.3008,
+ "allocatedBytes": 0
+ }
+ },
+ "UnitConversionBenchmarks\u003CDouble\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 6.8747,
+ "allocatedBytes": 0
+ }
+ },
+ "UnitConversionBenchmarks\u003CPreciseNumber\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 997.1396,
+ "allocatedBytes": 232
+ }
+ },
+ "UnitConversionBenchmarks\u003CSingle\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 8.6266,
+ "allocatedBytes": 0
+ }
+ },
+ "VectorBenchmarks\u003CDecimal\u003E.Length": {
+ "": {
+ "meanNs": 169.2199,
+ "allocatedBytes": 0
+ }
+ },
+ "VectorBenchmarks\u003CDouble\u003E.Length": {
+ "": {
+ "meanNs": 0.3754,
+ "allocatedBytes": 0
+ }
+ },
+ "VectorBenchmarks\u003CPreciseNumber\u003E.Length": {
+ "": {
+ "meanNs": 1780.1261,
+ "allocatedBytes": 312
+ }
+ },
+ "VectorBenchmarks\u003CSingle\u003E.Length": {
+ "": {
+ "meanNs": 0.7037,
+ "allocatedBytes": 0
+ }
+ }
+ }
+ },
+ {
+ "version": "5.2.0",
+ "commit": "eda9fc8",
+ "date": "2026-09-13",
+ "cpu": "Intel Xeon Processor 2.80GHz",
+ "runtime": ".NET 10.0.12 (10.0.12, 10.0.1226.42308)",
+ "baselineNs": 454.7423,
+ "runId": "local-seed",
+ "benchmarks": {
+ "ComparisonBenchmarks\u003CDecimal\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 19.0621,
+ "allocatedBytes": 32
+ }
+ },
+ "ComparisonBenchmarks\u003CDouble\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 13.6742,
+ "allocatedBytes": 24
+ }
+ },
+ "ComparisonBenchmarks\u003CPreciseNumber\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 28.8963,
+ "allocatedBytes": 40
+ }
+ },
+ "ComparisonBenchmarks\u003CSingle\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 13.1918,
+ "allocatedBytes": 24
+ }
+ },
+ "ConstructionBenchmarks\u003CDecimal\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 7.6477,
+ "allocatedBytes": 0
+ }
+ },
+ "ConstructionBenchmarks\u003CDouble\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 2.035,
+ "allocatedBytes": 0
+ }
+ },
+ "ConstructionBenchmarks\u003CPreciseNumber\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 106.7759,
+ "allocatedBytes": 40
+ }
+ },
+ "ConstructionBenchmarks\u003CSingle\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 0.8308,
+ "allocatedBytes": 0
+ }
+ },
+ "OperatorBenchmarks\u003CDecimal\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 49.7014,
+ "allocatedBytes": 0
+ }
+ },
+ "OperatorBenchmarks\u003CDouble\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 0.0023,
+ "allocatedBytes": 0
+ }
+ },
+ "OperatorBenchmarks\u003CPreciseNumber\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 175.7197,
+ "allocatedBytes": 48
+ }
+ },
+ "OperatorBenchmarks\u003CSingle\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 0.5194,
+ "allocatedBytes": 0
+ }
+ },
+ "UnitConversionBenchmarks\u003CDecimal\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 79.1666,
+ "allocatedBytes": 0
+ }
+ },
+ "UnitConversionBenchmarks\u003CDouble\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 22.796,
+ "allocatedBytes": 0
+ }
+ },
+ "UnitConversionBenchmarks\u003CPreciseNumber\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 637.7155,
+ "allocatedBytes": 232
+ }
+ },
+ "UnitConversionBenchmarks\u003CSingle\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 19.5547,
+ "allocatedBytes": 0
+ }
+ },
+ "VectorBenchmarks\u003CDecimal\u003E.Length": {
+ "": {
+ "meanNs": 506.5822,
+ "allocatedBytes": 0
+ }
+ },
+ "VectorBenchmarks\u003CDouble\u003E.Length": {
+ "": {
+ "meanNs": 0.9443,
+ "allocatedBytes": 0
+ }
+ },
+ "VectorBenchmarks\u003CPreciseNumber\u003E.Length": {
+ "": {
+ "meanNs": 6588.3068,
+ "allocatedBytes": 1760
+ }
+ },
+ "VectorBenchmarks\u003CSingle\u003E.Length": {
+ "": {
+ "meanNs": 0.9528,
+ "allocatedBytes": 0
+ }
+ }
+ }
+ },
+ {
+ "version": "5.2.4",
+ "commit": "cbc914c",
+ "date": "2026-09-14",
+ "cpu": "Intel Xeon Processor 2.80GHz",
+ "runtime": ".NET 10.0.12 (10.0.12, 10.0.1226.42308)",
+ "baselineNs": 454.7423,
+ "runId": "local-seed",
+ "benchmarks": {
+ "ComparisonBenchmarks\u003CDecimal\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 19.0401,
+ "allocatedBytes": 32
+ }
+ },
+ "ComparisonBenchmarks\u003CDouble\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 13.9626,
+ "allocatedBytes": 24
+ }
+ },
+ "ComparisonBenchmarks\u003CPreciseNumber\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 30.4436,
+ "allocatedBytes": 40
+ }
+ },
+ "ComparisonBenchmarks\u003CSingle\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 15.0078,
+ "allocatedBytes": 24
+ }
+ },
+ "ConstructionBenchmarks\u003CDecimal\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 15.3305,
+ "allocatedBytes": 0
+ }
+ },
+ "ConstructionBenchmarks\u003CDouble\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 3.3693,
+ "allocatedBytes": 0
+ }
+ },
+ "ConstructionBenchmarks\u003CPreciseNumber\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 119.6968,
+ "allocatedBytes": 40
+ }
+ },
+ "ConstructionBenchmarks\u003CSingle\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 2.0236,
+ "allocatedBytes": 0
+ }
+ },
+ "OperatorBenchmarks\u003CDecimal\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 48.7248,
+ "allocatedBytes": 0
+ }
+ },
+ "OperatorBenchmarks\u003CDouble\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 0.0535,
+ "allocatedBytes": 0
+ }
+ },
+ "OperatorBenchmarks\u003CPreciseNumber\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 168.3563,
+ "allocatedBytes": 48
+ }
+ },
+ "OperatorBenchmarks\u003CSingle\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 0.0685,
+ "allocatedBytes": 0
+ }
+ },
+ "UnitConversionBenchmarks\u003CDecimal\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 81.7809,
+ "allocatedBytes": 0
+ }
+ },
+ "UnitConversionBenchmarks\u003CDouble\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 20.0893,
+ "allocatedBytes": 0
+ }
+ },
+ "UnitConversionBenchmarks\u003CPreciseNumber\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 634.1803,
+ "allocatedBytes": 232
+ }
+ },
+ "UnitConversionBenchmarks\u003CSingle\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 17.9519,
+ "allocatedBytes": 0
+ }
+ },
+ "VectorBenchmarks\u003CDecimal\u003E.Length": {
+ "": {
+ "meanNs": 515.3732,
+ "allocatedBytes": 0
+ }
+ },
+ "VectorBenchmarks\u003CDouble\u003E.Length": {
+ "": {
+ "meanNs": 0.8839,
+ "allocatedBytes": 0
+ }
+ },
+ "VectorBenchmarks\u003CPreciseNumber\u003E.Length": {
+ "": {
+ "meanNs": 6395.2321,
+ "allocatedBytes": 1760
+ }
+ },
+ "VectorBenchmarks\u003CSingle\u003E.Length": {
+ "": {
+ "meanNs": 1.0075,
+ "allocatedBytes": 0
+ }
+ }
+ }
+ },
+ {
+ "version": "5.3.2",
+ "commit": "2a3e53b",
+ "date": "2026-09-16",
+ "cpu": "Intel Xeon Processor 2.80GHz",
+ "runtime": ".NET 10.0.12 (10.0.12, 10.0.1226.42308)",
+ "baselineNs": 454.7423,
+ "runId": "local-seed",
+ "benchmarks": {
+ "ComparisonBenchmarks\u003CDecimal\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 18.7366,
+ "allocatedBytes": 32
+ }
+ },
+ "ComparisonBenchmarks\u003CDouble\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 13.152,
+ "allocatedBytes": 24
+ }
+ },
+ "ComparisonBenchmarks\u003CPreciseNumber\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 28.5365,
+ "allocatedBytes": 40
+ }
+ },
+ "ComparisonBenchmarks\u003CSingle\u003E.CompareToInterface": {
+ "": {
+ "meanNs": 13.502,
+ "allocatedBytes": 24
+ }
+ },
+ "ConstructionBenchmarks\u003CDecimal\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 15.5335,
+ "allocatedBytes": 0
+ }
+ },
+ "ConstructionBenchmarks\u003CDouble\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 3.3758,
+ "allocatedBytes": 0
+ }
+ },
+ "ConstructionBenchmarks\u003CPreciseNumber\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 119.0305,
+ "allocatedBytes": 40
+ }
+ },
+ "ConstructionBenchmarks\u003CSingle\u003E.FromNauticalMile": {
+ "": {
+ "meanNs": 2.0081,
+ "allocatedBytes": 0
+ }
+ },
+ "OperatorBenchmarks\u003CDecimal\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 48.593,
+ "allocatedBytes": 0
+ }
+ },
+ "OperatorBenchmarks\u003CDouble\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 0.5216,
+ "allocatedBytes": 0
+ }
+ },
+ "OperatorBenchmarks\u003CPreciseNumber\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 169.8841,
+ "allocatedBytes": 48
+ }
+ },
+ "OperatorBenchmarks\u003CSingle\u003E.LengthTimesLength": {
+ "": {
+ "meanNs": 0.0431,
+ "allocatedBytes": 0
+ }
+ },
+ "UnitConversionBenchmarks\u003CDecimal\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 82.7255,
+ "allocatedBytes": 0
+ }
+ },
+ "UnitConversionBenchmarks\u003CDouble\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 17.6686,
+ "allocatedBytes": 0
+ }
+ },
+ "UnitConversionBenchmarks\u003CPreciseNumber\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 631.3755,
+ "allocatedBytes": 232
+ }
+ },
+ "UnitConversionBenchmarks\u003CSingle\u003E.InNauticalMile": {
+ "": {
+ "meanNs": 18.8093,
+ "allocatedBytes": 0
+ }
+ },
+ "VectorBenchmarks\u003CDecimal\u003E.Length": {
+ "": {
+ "meanNs": 512.9149,
+ "allocatedBytes": 0
+ }
+ },
+ "VectorBenchmarks\u003CDouble\u003E.Length": {
+ "": {
+ "meanNs": 1.0452,
+ "allocatedBytes": 0
+ }
+ },
+ "VectorBenchmarks\u003CPreciseNumber\u003E.Length": {
+ "": {
+ "meanNs": 6547.0307,
+ "allocatedBytes": 1760
+ }
+ },
+ "VectorBenchmarks\u003CSingle\u003E.Length": {
+ "": {
+ "meanNs": 1.0098,
+ "allocatedBytes": 0
+ }
+ }
+ }
+ }
+ ]
+}
diff --git a/docs/benchmarks/performance-dark.svg b/docs/benchmarks/performance-dark.svg
new file mode 100644
index 0000000..406755d
--- /dev/null
+++ b/docs/benchmarks/performance-dark.svg
@@ -0,0 +1,263 @@
+
diff --git a/docs/benchmarks/performance.svg b/docs/benchmarks/performance.svg
new file mode 100644
index 0000000..90a9ea9
--- /dev/null
+++ b/docs/benchmarks/performance.svg
@@ -0,0 +1,263 @@
+
diff --git a/scripts/benchmark-history.cs b/scripts/benchmark-history.cs
new file mode 100644
index 0000000..c1161f0
--- /dev/null
+++ b/scripts/benchmark-history.cs
@@ -0,0 +1,634 @@
+// Copyright (c) 2023-2026 ktsu-dev contributors
+
+// Accumulates benchmark results per release and draws them for the README.
+//
+// dotnet run scripts/benchmark-history.cs -- ingest --history --results --version
+// dotnet run scripts/benchmark-history.cs -- render --history --out ");
+ return svg.ToString();
+ }
+
+ private static void Preamble(StringBuilder svg, Theme theme, int width, int height, JsonArray entries)
+ {
+ svg.AppendLine(CultureInfo.InvariantCulture, $"""