-
Notifications
You must be signed in to change notification settings - Fork 0
Benchmark
Performance and correctness benchmarks for DiGi.Geometry.
All benchmarks live as [Fact] tests in the DiGi.Geometry.xUnit test project
(DiGi.Test/DiGi.Geometry.xUnit/Facts/), so they can be re-run and re-verified at any time rather
than treated as a one-off snapshot.
| Component | Specification |
|---|---|
| CPU | AMD Ryzen 9 9950X, 16 cores / 32 threads (Environment.ProcessorCount = 32) |
| RAM | 61.4 GB |
| OS | Windows 11 Pro (10.0.26200) |
| .NET SDK | 10.0.301 |
| Build config | Release (unless noted) |
Numbers are machine-specific — re-run the benchmarks on your own hardware before drawing conclusions for a different environment.
File: Facts/OptimizationPerformance.cs
Methods compared:
-
DiGi.Geometry.Planar.Query.Average(IEnumerable<Point2D>)— the vertex mean center,C = (1/n) · Σ Pᵢ; a single pass accumulatingx/y(≈ 2 additions per vertex). -
DiGi.Geometry.Planar.Query.Centroid(IEnumerable<Point2D>)— the area centroid (polygon centre of mass),C = 1/(6A) · Σ (Pᵢ + Pᵢ₊₁)(xᵢyᵢ₊₁ − xᵢ₊₁yᵢ); the shoelace-weighted sum (≈ 4 multiply-adds per vertex).
Both are O(n) single-pass queries. The benchmark sweeps the point count over
{100, 1 000, 10 000, 100 000, 1 000 000} — a regular polygon inscribed in a circle of radius 10
centred on the origin — and reports the per-call time at each scale. A warm-up call precedes the
measured loop (JIT), and the number of repeats is scaled inversely with the point count
(repeats = Max(1, 2 000 000 / count)) so the total measured work stays comparable across scales.
Each scale also asserts both queries resolve to the origin (correctness), so the timings compare
equivalent work.
Context: DiGi.Geometry.Planar.Query.InternalPoint uses the area centroid as its primary
internal-point candidate and the vertex mean (Average) as the intermediate fallback before the
NetTopologySuite InteriorPoint path — this benchmark quantifies the cost of that fallback.
| points | Average (µs/call) | Centroid (µs/call) | ratio (Centroid / Average) |
|---|---|---|---|
| 100 | 3.38 | 2.76 | 0.82× |
| 1,000 | 28.59 | 26.50 | 0.93× |
| 10,000 | 48.10 | 56.64 | 1.18× |
| 100,000 | 251.16 | 709.93 | 2.83× |
| 1,000,000 | 2,452.20 | 3,984.75 | 1.62× |
| points | Average (µs/call) | Centroid (µs/call) | ratio (Centroid / Average) |
|---|---|---|---|
| 100 | 1.33 | 2.42 | 1.82× |
| 1,000 | 12.83 | 22.80 | 1.78× |
| 10,000 | 129.11 | 224.17 | 1.74× |
| 100,000 | 1,304.80 | 2,629.66 | 2.02× |
| 1,000,000 | 12,171.55 | 20,470.20 | 1.68× |
-
Both scale linearly — per-call time grows ~10× for each 10× increase in point count, confirming
the
O(n)single-pass behaviour of both queries with no scale-dependent surprise. -
Averagedoes strictly less work per vertex — ~2 additions versus the shoelace sum's ~4 multiply-adds — and, for aList<Point2D>input, it also avoids the internalToArray()copy thatCentroidincurs (itspoint2Ds as Point2D[]array fast-path misses for aList). SoAverageis both fewer FLOPs and one fewerO(n)allocation. -
Debug (no JIT optimisation) shows that raw work gap uniformly:
Averageis ~1.7–2× faster at every size. -
Release (JIT-optimised) tells a more nuanced story. At tiny inputs (≤ 1 000 points) both finish
in a few µs and per-call enumeration / measurement overhead dominates, so the ratio is noise and
can even invert (~0.8–0.9×).
Average's advantage emerges in the mid-range (1.18× at 10 000, ~2.8× at 100 000) and stays positive at 1 000 000 (~1.5–2×), though that top row is the noisiest — only 2 timed repeats, run-to-run variance ≈ ±20%. -
Takeaway for
InternalPoint: on realistic polygons both centres are trivially cheap (single-digit µs), so the vertex mean is essentially free to try as an intermediate fallback. It is chosen there for correctness/robustness — being a convex combination of the vertices it lands strictly inside more often — and the modest constant-factor speed edge is a bonus, not the reason. The centroid is not slow; the ordering is a quality decision, and the timings just confirm the extra fallback adds negligible cost.
A fixed-size companion test, AverageAndCentroid_PerformanceComparison (same file, 10 000 points),
asserts both queries agree on the origin and that Average is not slower than Centroid beyond a
generous noise margin, guarding against a future regression that would invert this relationship.
- Add a
[Fact]underFacts/inDiGi.Geometry.xUnit, following the repo's test conventions (partial class Facts, XML<summary>, explicit types, warm-up-then-Stopwatch). - Expose the sweep size(s) as a single
const/static readonlyfield at the top of the class so the benchmark can be scaled up for a stress run and back down for fast everyday runs. - Warm up (small input) before timing to exclude JIT cost.
- Assert cross-implementation agreement (result values / counts) so timing covers equivalent work.
- Run in Release for representative numbers, and capture the current machine spec.
- Record the machine spec, fully namespace-qualified method names, the result table(s), and the analysis on this page.
See also: the DiGi.ComputeSharp Benchmark page (GPU vs. CPU), which follows the same standard.