Skip to content

Improve polygonize performance: single-pass tracing, JIT merge helpers, batch shapely#1010

Merged
brendancol merged 3 commits intomasterfrom
issue-1008
Mar 16, 2026
Merged

Improve polygonize performance: single-pass tracing, JIT merge helpers, batch shapely#1010
brendancol merged 3 commits intomasterfrom
issue-1008

Conversation

@brendancol
Copy link
Contributor

@brendancol brendancol commented Mar 15, 2026

Summary

Two internal performance changes to polygonize. No API changes.

_simplify_ring, _signed_ring_area, and _point_in_ring were plain Python loops in the dask chunk-merge path. Added @ngjit so they compile with numba.

_to_geopandas now batch-constructs hole-free polygons via shapely.linearrings() + shapely.polygons() on shapely 2.0+. Polygons with holes and older shapely fall back to the scalar constructor.

(An earlier commit tried replacing the two-pass _follow with a single-pass buffer-growth approach. Benchmarks showed it was 15-30% slower -- numba already optimizes the two-pass loop well, and the buffer growth check added inner-loop overhead. Reverted.)

Benchmarks

Benchmark Master PR Change
Tracing (unchanged)
numpy 300x150 5.10 ms 5.19 ms ~same
numpy 1000x500 79.47 ms 80.36 ms ~same
JIT merge helpers
_point_in_ring 10k pts 5.61 ms 0.01 ms 561x
_simplify_ring 5k pts 1.51 ms 0.01 ms 151x
Dask end-to-end
300x150, chunks=(30,30) 1198 ms 471 ms 2.5x
300x150, chunks=(15,15) 2592 ms 1121 ms 2.3x
1000x500, chunks=(50,50) 89.7 s 36.3 s 2.5x
Geopandas output
300x150 127 ms 78 ms 1.6x
1000x500 1423 ms 970 ms 1.5x

Test plan

  • All 74 existing polygonize tests pass (numpy, dask, cupy, dask+cupy, all output formats)
  • Direct validation of JIT-compiled _simplify_ring, _signed_ring_area, _point_in_ring
  • Checkerboard in small dask chunks forces many boundary merges through JIT path
  • Geopandas output with mixed hole-free and holed polygons through batch shapely path

Closes #1008

…s, batch shapely (#1008)

Replace the two-pass _follow with a single-pass implementation using a
dynamically-grown buffer. This eliminates retracing every polygon
boundary a second time, which was the dominant cost for rasters with
many small regions.

Add @ngjit to _point_in_ring, _simplify_ring, and _signed_ring_area
so the dask chunk-merge path runs compiled instead of interpreted.

Use shapely.polygons() batch constructor for hole-free polygons in
_to_geopandas (shapely 2.0+, with fallback for older versions).
- Buffer growth: snake-shaped polygon with >64 boundary points
- JIT merge helpers: direct tests of _simplify_ring, _signed_ring_area,
  _point_in_ring
- Dask merge: checkerboard pattern forcing many boundary merges
- Geopandas batch: mixed hole-free and holed polygons through the
  shapely.polygons() batch path
@github-actions github-actions bot added the performance PR touches performance-sensitive code label Mar 15, 2026
…1008)

Benchmarks showed the single-pass _follow was 15-30% slower than the
original two-pass version. The buffer growth check in the inner loop
adds overhead that numba doesn't optimize away, and the two-pass
approach benefits from the data being in cache on the second pass.

Reverted _follow to the original two-pass implementation. The JIT
merge helpers (2.3-2.6x dask speedup) and batch shapely construction
(1.3-1.6x geopandas speedup) are kept.
@brendancol brendancol merged commit 907a3cd into master Mar 16, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

performance PR touches performance-sensitive code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Improve polygonize performance: single-pass tracing, JIT merge helpers, batch shapely

1 participant