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