Skip to content

fix: correctness fixes; performance regressions - #142

Merged
pocketken merged 2 commits into
mainfrom
dev/km/post-v4.5.0-tuning
Jul 15, 2026
Merged

fix: correctness fixes; performance regressions#142
pocketken merged 2 commits into
mainfrom
dev/km/post-v4.5.0-tuning

Conversation

@pocketken

Copy link
Copy Markdown
Owner

A performance and allocation pass on top of 4.5.0, plus new zero-allocation span APIs and a parallel polyfill. No upstream H3 API changes; cell and index results remain identical to 4.5.0 (the projection fix below shifts geometry by a few ULP to match libh3 exactly).

Fixes 🔧
  • the spherical inverse projection (ToLatLng, GetCellBoundary and its vertices, directed-edge boundaries, and everything built on them) now matches upstream libh3 v4.5.0 bit-for-bit. v4.5.0 replaced the spherical-law-of-cosines inverse with a 3D-vector construction (tangent basis + linear combination + normalize); the port now follows it in the same operation order. Center and boundary coordinates shift by a few ULP from earlier 4.5.0 builds
  • fixes a codegen regression on net10.0 in GetDirectNeighbour/GetDirectNeighbourWithoutRotations: over-aggressive inlining of the generated per-digit rotation tables gave the method a large zero-initialized stack frame, making grid disk, grid ring, IsNeighbour, directed-edge and polyfill operations several times slower under the net10 JIT than under net8. net10 is now at parity with net8 on these paths (net8 was unaffected)
  • Fill now traces the polygon boundary in cells before flooding inward, matching upstream polygonToCells seeding. Thin/narrow features, holes and antimeridian-crossing shapes that are contained but only reachable through non-contained neighbours (and so were previously dropped from a single interior seed) are now returned. Locked to libh3's exact cell set by a differential corpus (thin sliver, concave L, box-with-hole, antimeridian quad, disjoint multipolygon)
Enhancements 🎉
  • new zero-allocation span/buffer-fill overloads across the traversal, hierarchy and set APIs: GridDisk, GridDiskDistances, GridRingUnsafe, GridPathCells, GetChildrenForResolution, CompactCells, UncompactCells and GetCellBoundaryVertices gain overloads that fill a caller-owned Span<T> and return the number of cells written, with sizing helpers MaxGridDiskSize, MaxGridRingSize, GridPathCellsSize, CellToChildrenSize and UncompactCellsSize. Allocation-free on a warm ArrayPool; the streaming IEnumerable APIs are unchanged and produce identical results
  • new ParallelFill for large polygon fills: shards the polygon's envelope into horizontal strips, fills each concurrently and unions the result. Produces the same (unordered) cell set as Fill. It is opt-in and only pays off on large fills: it trades the sequential fill's flat allocation for wall-clock, and below a few thousand output cells the setup cost makes it slower than Fill
Performance 🚀
  • the forward and inverse spherical projections (FromLatLng/ToLatLng, cell boundaries, areas, edge lengths) drop a large amount of redundant transcendental work while staying bit-for-bit with the reference: precomputed per-face-center sin/cos and axis-azimuth tables, angle-subtraction identities in place of per-call atan2/sin/cos, a cancelled cos(latitude) in the longitude solve, a collapsed planar-radius sqrt, precomputed M_SQRT7 powers, integer round-div-by-7 on the aperture-7 up-scaling chain, and per-vertex trig cached across the Cagnoli area loop
  • aperture-7 digit-pair down-steps in the resolution walk are fused into a single normalization, and ToFaceIJK fuses the aperture-7 down-step with the neighbour walk; GetDirectNeighbour fast-paths the common no-base-cell-crossing tail and skips the identity rotation reorient
  • polyfill flood fill replaces NTS's per-call allocating point-in-area locator with an allocation-free inline ray-crossing locator, replaces its HashSet/Stack working sets with pooled open-addressed structures presized from the geometry's area, and hoists per-cell invariants out of the neighbour walk
  • grid disk, cell boundary and edge length drop iterator and intermediate-array allocations via eager span fills and stackalloc buffers; CompactCells uses a presized bucket and a primitive ulong sort, and UncompactCells uses a pooled open-addressed dedup table and an inlined child walk
Testing
  • geometry parity is gated per operation against authoritative libh3 v4.5.0: per-cell outputs within 1 ULP and the whole-sphere cell-area sum within 3 ULP, with a persisted manifest recording the worst per-op ULP observed (index and cell outputs match exactly). Added adversarial Vec3d edge-case coverage and per-op ULP diagnostics

@pocketken
pocketken merged commit a108267 into main Jul 15, 2026
1 check passed
@pocketken
pocketken deleted the dev/km/post-v4.5.0-tuning branch July 15, 2026 05:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants