From cb2524980d70af61e04949f0a8d80c4399caa6fc Mon Sep 17 00:00:00 2001 From: just Date: Mon, 14 Sep 2026 00:04:39 +0400 Subject: [PATCH] basic arithmetic benchmarks added --- .gitea/workflows/test-dotnet.yaml | 5 + .../ArithmeticBenchmarks.cs | 107 ++++++++++++++++++ .../Just.PreciseMath.Benchmarks.csproj | 5 +- .../Just.PreciseMath.Benchmarks/Program.cs | 31 +++++ AGENTS.md | 4 +- README.md | 43 ++++++- 6 files changed, 186 insertions(+), 9 deletions(-) create mode 100644 2-benchmarks/Just.PreciseMath.Benchmarks/ArithmeticBenchmarks.cs create mode 100644 2-benchmarks/Just.PreciseMath.Benchmarks/Program.cs diff --git a/.gitea/workflows/test-dotnet.yaml b/.gitea/workflows/test-dotnet.yaml index 78544fa..cc7329c 100644 --- a/.gitea/workflows/test-dotnet.yaml +++ b/.gitea/workflows/test-dotnet.yaml @@ -49,6 +49,11 @@ jobs: --report-xunit-trx --coverlet --coverlet-output-format cobertura --coverlet-include "[Just.PreciseMath]*" + - name: Benchmark smoke test + run: >- + dotnet run --project 2-benchmarks/Just.PreciseMath.Benchmarks + --configuration Release --no-build -- --job Dry --filter '*' + - name: Upload test and coverage results uses: actions/upload-artifact@v7 if: ${{ always() }} diff --git a/2-benchmarks/Just.PreciseMath.Benchmarks/ArithmeticBenchmarks.cs b/2-benchmarks/Just.PreciseMath.Benchmarks/ArithmeticBenchmarks.cs new file mode 100644 index 0000000..8da7594 --- /dev/null +++ b/2-benchmarks/Just.PreciseMath.Benchmarks/ArithmeticBenchmarks.cs @@ -0,0 +1,107 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; + +namespace Just.PreciseMath.Benchmarks; + +/// Compares same-type scalar arithmetic on ordinary finite operands. +[MemoryDiagnoser] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +[CategoriesColumn] +public class ArithmeticBenchmarks +{ + private double _doubleLeft; + private double _doubleRight; + private decimal _decimalLeft; + private decimal _decimalRight; + private DoubleDouble _doubleDoubleLeft; + private DoubleDouble _doubleDoubleRight; + + /// One binary-exact input and one decimal input with a binary residual. + [Params(1.25, 1.1)] + public double Left { get; set; } + + [GlobalSetup] + public void Setup() + { + // Conversion and construction are deliberately outside the timed methods. + // Decimal and DoubleDouble represent the same decimal input; double rounds it. + _doubleLeft = Left; + _doubleRight = 0.75; + _decimalLeft = (decimal)Left; + _decimalRight = 0.75m; + _doubleDoubleLeft = (DoubleDouble)_decimalLeft; + _doubleDoubleRight = (DoubleDouble)_decimalRight; + } + + [Benchmark(Baseline = true), BenchmarkCategory("Addition")] + public double DoubleAdd() + { + return _doubleLeft + _doubleRight; + } + + [Benchmark, BenchmarkCategory("Addition")] + public decimal DecimalAdd() + { + return _decimalLeft + _decimalRight; + } + + [Benchmark, BenchmarkCategory("Addition")] + public DoubleDouble DoubleDoubleAdd() + { + return _doubleDoubleLeft + _doubleDoubleRight; + } + + [Benchmark(Baseline = true), BenchmarkCategory("Subtraction")] + public double DoubleSubtract() + { + return _doubleLeft - _doubleRight; + } + + [Benchmark, BenchmarkCategory("Subtraction")] + public decimal DecimalSubtract() + { + return _decimalLeft - _decimalRight; + } + + [Benchmark, BenchmarkCategory("Subtraction")] + public DoubleDouble DoubleDoubleSubtract() + { + return _doubleDoubleLeft - _doubleDoubleRight; + } + + [Benchmark(Baseline = true), BenchmarkCategory("Multiplication")] + public double DoubleMultiply() + { + return _doubleLeft * _doubleRight; + } + + [Benchmark, BenchmarkCategory("Multiplication")] + public decimal DecimalMultiply() + { + return _decimalLeft * _decimalRight; + } + + [Benchmark, BenchmarkCategory("Multiplication")] + public DoubleDouble DoubleDoubleMultiply() + { + return _doubleDoubleLeft * _doubleDoubleRight; + } + + [Benchmark(Baseline = true), BenchmarkCategory("Division")] + public double DoubleDivide() + { + return _doubleLeft / _doubleRight; + } + + [Benchmark, BenchmarkCategory("Division")] + public decimal DecimalDivide() + { + return _decimalLeft / _decimalRight; + } + + [Benchmark, BenchmarkCategory("Division")] + public DoubleDouble DoubleDoubleDivide() + { + return _doubleDoubleLeft / _doubleDoubleRight; + } +} diff --git a/2-benchmarks/Just.PreciseMath.Benchmarks/Just.PreciseMath.Benchmarks.csproj b/2-benchmarks/Just.PreciseMath.Benchmarks/Just.PreciseMath.Benchmarks.csproj index f344696..3503c0a 100644 --- a/2-benchmarks/Just.PreciseMath.Benchmarks/Just.PreciseMath.Benchmarks.csproj +++ b/2-benchmarks/Just.PreciseMath.Benchmarks/Just.PreciseMath.Benchmarks.csproj @@ -2,9 +2,8 @@ Just.PreciseMath.Benchmarks Just.PreciseMath.Benchmarks - BenchmarkDotNet scaffold for measuring Just.PreciseMath arithmetic and mathematical functions. - - Library + BenchmarkDotNet comparisons of DoubleDouble, decimal, and double arithmetic. + Exe diff --git a/2-benchmarks/Just.PreciseMath.Benchmarks/Program.cs b/2-benchmarks/Just.PreciseMath.Benchmarks/Program.cs new file mode 100644 index 0000000..a6c0586 --- /dev/null +++ b/2-benchmarks/Just.PreciseMath.Benchmarks/Program.cs @@ -0,0 +1,31 @@ +using BenchmarkDotNet.Reports; +using BenchmarkDotNet.Running; + +namespace Just.PreciseMath.Benchmarks; + +internal static class Program +{ + private static int Main(string[] args) + { + BenchmarkSwitcher switcher = BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly); + // Only these standalone discovery/help forms intentionally produce no reports. + if (args is ["--list", "flat" or "tree"] or ["--help"] or ["-h"]) + { + switcher.Run(args); + return 0; + } + + Summary[] summaries = switcher.Run(args.Length == 0 ? ["--filter", "*"] : args).ToArray(); + // BenchmarkDotNet can return normally after validation, build, or execution + // failures. Do not let CI mistake an empty or failed run for a passing smoke test. + if (summaries.Length == 0 || summaries.Any(summary => + summary.HasCriticalValidationErrors || summary.Reports.Length == 0 || + summary.Reports.Any(report => !report.Success || report.ResultStatistics is null))) + { + Console.Error.WriteLine("Benchmark run failed or produced no measurements."); + return 1; + } + + return 0; + } +} diff --git a/AGENTS.md b/AGENTS.md index ff75047..a8bb0db 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,8 +12,8 @@ Do not claim API completeness or accuracy beyond tested contracts. - `0-source/Just.PreciseMath/`: library implementation and package metadata. - `1-tests/Just.PreciseMath.Tests/`: xUnit v3 tests, Shouldly assertions, and Microsoft.Testing.Platform (MTP) with `coverlet.MTP` coverage. -- `2-benchmarks/Just.PreciseMath.Benchmarks/`: source-free BenchmarkDotNet library - scaffold, not yet a runnable benchmark suite. +- `2-benchmarks/Just.PreciseMath.Benchmarks/`: executable BenchmarkDotNet arithmetic + comparisons; use the Dry job for smoke validation, not performance conclusions. - Root `Directory.Build.props` holds shared settings. Each numbered directory's props explicitly imports it; preserve this import chain. - `review-legacy/`: optional local reference material, excluded by `.gitignore`. diff --git a/README.md b/README.md index 8ecc1bc..4bc5576 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ rounded 106-bit results**. The deterministic rational-oracle tests check a conservative error bound of `2^-100` relative plus one minimum binary64 subnormal, with exact component checks for selected representable cases. Near underflow, extended precision necessarily decreases; overflow produces infinity. Performance -has not been benchmarked, including the allocating exponent-boundary path. +of the allocating exponent-boundary path is not covered by the basic benchmarks. ## Conversions and formatting @@ -133,8 +133,8 @@ bool success = DoubleDouble.TryParse("1.25e-2".AsSpan(), CultureInfo.InvariantCu The planned `PreciseMath` static class and its `Abs`, `Sqrt`, `Pow`, `Exp`, and `Log` functions are not implemented. Generic-math interfaces beyond `ISignedNumber`, -additional text formats/general round-trip formatting, and performance benchmarks -remain deferred. +additional text formats/general round-trip formatting, and broader performance +benchmarks remain deferred. Replacing allocating arithmetic boundary fallbacks is also deferred; the current `BigInteger` paths remain in place. That optimization does not require removing `BigInteger` from conversions, parsing, formatting, or independent test oracles. @@ -154,11 +154,46 @@ dotnet format Just.PreciseMath.slnx --verify-no-changes --no-restore Test results and coverage reports are available in the `test-results` artifact on CI workflow runs. +## Benchmarks + +The BenchmarkDotNet suite compares same-type `+`, `-`, `*`, and `/` operations for +`DoubleDouble`, `decimal`, and `double`: 12 methods with two input cases each. +Each operation/input group uses `double` as its baseline and reports allocations. +Operands are stored in fields and results are returned to the harness; conversions +and construction happen in setup, outside the timed methods. + +The left inputs are `1.25` (binary-exact) and `1.1` (a nonzero low component in +`DoubleDouble`); the right input is `0.75`. Decimal and double-double inputs originate +from the same decimal values, while `double` rounds to binary64. These types have +different precision and range contracts: this is a cost comparison, not an accuracy +test. Exceptional values, exponent-boundary fallbacks, mixed-type operators, +conversions, parsing, and formatting are not benchmarked yet. + +After the Release build above, run from the repository root: + +```sh +# Discover benchmark methods without running them. +dotnet run --project 2-benchmarks/Just.PreciseMath.Benchmarks -c Release --no-build -- --list flat + +# Quick execution check (also run in CI); not useful for timing comparisons. +dotnet run --project 2-benchmarks/Just.PreciseMath.Benchmarks -c Release --no-build -- --job Dry --filter '*' + +# Full measurement run; optionally select an operation with --anyCategories Addition. +dotnet run --project 2-benchmarks/Just.PreciseMath.Benchmarks -c Release --no-build -- --filter '*' +``` + +Reports are written under the ignored `BenchmarkDotNet.Artifacts/` directory. +Empty selections and failed benchmark runs return a nonzero exit code. CI only +checks execution, with no performance gate. Use full runs on an idle, controlled +machine for comparisons; a scalar `double` operation may approach harness overhead, +so inspect BenchmarkDotNet warnings before interpreting ratios. Do not compare +coverage-instrumented runs or treat Dry-job timings as performance measurements. + ## Project structure - `0-source/Just.PreciseMath/`: library implementation. - `1-tests/Just.PreciseMath.Tests/`: unit tests. -- `2-benchmarks/Just.PreciseMath.Benchmarks/`: reserved for performance benchmarks. +- `2-benchmarks/Just.PreciseMath.Benchmarks/`: arithmetic benchmarks against `decimal` and `double`. ## Contributing