Performance¶
These historical measurements predate the current dependency and algorithm updates and have not been rerun for this revision.
The numbers below were collected on Linux x86_64, Intel i7-10700K, release build, Python 3.13.5, and NumPy 2.4.2. Reproduce with scripts/benchmark.py.
For a new run, save measurements and environment versions with uv run python scripts/benchmark.py --output results.json.
Use --measure-process-rss to include retained process RSS measurements.
API differences vs rasterio.features.shapes¶
These benchmarks compare contourrs to rasterio.features.shapes, which is the main public Python baseline for polygonization.
| Feature | rasterio.features.shapes |
contourrs |
|---|---|---|
| Input types | ndarray, raster band, or dataset-backed band tuple |
ndarray only |
| Output form | Iterator of (geometry, value) pairs |
Eager list[...] via shapes() or pyarrow.Table via shapes_arrow() |
| Nodata handling | Explicit mask; dataset/band workflows can also bring nodata metadata from the surrounding stack |
Explicit mask or nodata= on ndarray inputs |
| Dtypes | int8, int16, int32, uint8, uint16, float32, float64 |
uint8, uint16, uint32, int16, int32, float32, float64 |
| Transform input | Affine |
Affine or 6-tuple (a, b, c, d, e, f) |
| Arrow / GeoParquet-ready output | None | Built in via shapes_arrow() |
If you already have a NumPy array in memory, this is the closest apples-to-apples comparison. If you need dataset IO, metadata propagation, or format conversion, the broader rasterio/GDAL stack still covers more ground.
Methodology¶
- Timing — median of 5 runs after 2 warmup runs (
3 + 1for the real-world cases) - Synthetic polygonize — random
uint8raster with 5 categorical values; intentionally noisy, high-boundary workload - Synthetic contours — random
float32raster with 5 thresholds; near worst-case for marching-squares ring count - Python heap — peak allocation from
tracemalloc - Retained process RSS delta — fresh-subprocess RSS after the call, with its result retained, minus a post-import, post-input-load baseline; this is not peak RSS
Warning
tracemalloc measures Python-managed heap only. It does not capture Rust heap allocations, Arrow/native buffers, or total process memory. shapes_arrow() and contours_arrow() reduce Python object construction, but they still allocate substantial native memory.
Polygonization of synthetic rasters¶
rasterio.features.shapes is the main public Python baseline for shapes() and shapes_arrow(). Internally it wraps GDAL's polygonize path, but the numbers here are intended as an apples-to-apples Python API comparison.
| Grid | shapes() |
shapes_arrow() |
rasterio | arrow() vs shapes() |
arrow() vs rasterio |
|---|---|---|---|---|---|
| 64x64 | 3.8ms | 873us | 8.0ms | 4.4x | 9.2x |
| 128x128 | 17.9ms | 3.9ms | 32.3ms | 4.6x | 8.3x |
| 256x256 | 88.2ms | 15.8ms | 147.5ms | 5.6x | 9.4x |
| 512x512 | 371.7ms | 62.8ms | 622.0ms | 5.9x | 9.9x |
| 1024x1024 | 1.50s | 377.4ms | 2.51s | 4.0x | 6.7x |
| 2048x2048 | 6.17s | 1.63s | 9.97s | 3.8x | 6.1x |
shapes_arrow() avoids Python GeoJSON dict/list construction and passes WKB buffers to PyArrow.
Polygonization of CDL¶
512x512 crop of the 2023 USDA Cropland Data Layer for Polk County, Iowa.
| Dataset | shapes() |
shapes_arrow() |
rasterio | output rows |
|---|---|---|---|---|
| CDL 2023 Polk County 512x512 | 102.0ms | 23.1ms | 174.1ms | 34,130 |
On real land-cover data, shapes_arrow() is still about 4.4x faster than shapes() and about 7.5x faster than rasterio.
Contouring of synthetic rasters¶
These are contours() vs contours_arrow() on random float32 data with 5 thresholds. This is intentionally harsh on isoband extraction because it maximizes tiny rings.
| Grid | contours() |
contours_arrow() |
arrow() vs contours() |
|---|---|---|---|
| 64x64 | 3.8ms | 2.9ms | 1.3x |
| 128x128 | 21.0ms | 17.4ms | 1.2x |
| 256x256 | 172.0ms | 154.9ms | 1.1x |
| 512x512 | 2.08s | 1.87s | 1.1x |
| 1024x1024 | 33.98s | 32.46s | 1.0x |
The contour path spends most of its time in marching squares and polygon assembly, so Arrow helps less than it does for categorical polygonization.
Contouring of a DEM¶
2048x2048 Mt. Rainier DEM with 250 m contour bands.
| Dataset | contours() |
contours_arrow() |
output rows |
|---|---|---|---|
| Mt. Rainier DEM 2048x2048 | 49.8ms | 47.7ms | 186 |
Real DEMs are much smoother than random noise, so contour extraction is faster than the synthetic worst case.
Polygonization memory¶
Python heap peak (tracemalloc)¶
| Grid | shapes() |
shapes_arrow() |
rasterio | arrow() reduction vs rasterio |
|---|---|---|---|---|
| 64x64 | 2.7MB | <0.1MB | 2.6MB | 100% |
| 256x256 | 41.6MB | <0.1MB | 39.9MB | 100% |
| 512x512 | 166.4MB | <0.1MB | 159.4MB | 100% |
| 1024x1024 | 665.1MB | <0.1MB | 637.1MB | 100% |
| 2048x2048 | 2.60GB | <0.1MB | 2.49GB | 100% |
The <0.1MB entries measure Python-managed allocations, not the native buffers that hold the Arrow output.
Retained process RSS delta (fresh subprocess)¶
| Grid | shapes() |
shapes_arrow() |
rasterio | arrow() reduction vs rasterio |
|---|---|---|---|---|
| 64x64 | 5.5MB | 2.7MB | 12.7MB | 79% |
| 256x256 | 90.2MB | 10.6MB | 72.0MB | 85% |
| 512x512 | 358.5MB | 184.8MB | 256.9MB | 28% |
| 1024x1024 | 1.39GB | 714.0MB | 1002.8MB | 29% |
| 2048x2048 | 5.58GB | 2.79GB | 3.90GB | 28% |
shapes_arrow() allocates native memory for labeling, polygon tracing, WKB serialization, and Arrow buffers.
Its retained process RSS is ~28-29% lower than rasterio on the larger grids in this benchmark.
These before-and-after measurements do not capture peak memory during the call.
Contouring memory¶
Python heap peak (tracemalloc)¶
| Grid | contours() |
contours_arrow() |
arrow() reduction |
|---|---|---|---|
| 64x64 | 0.9MB | <0.1MB | 100% |
| 128x128 | 3.6MB | <0.1MB | 100% |
| 256x256 | 14.5MB | <0.1MB | 100% |
| 512x512 | 57.4MB | <0.1MB | 100% |
| 1024x1024 | 228.4MB | <0.1MB | 100% |
Retained process RSS delta (fresh subprocess)¶
| Grid | contours() |
contours_arrow() |
arrow() reduction |
|---|---|---|---|
| 64x64 | 2.0MB | 1.8MB | 8% |
| 128x128 | 8.6MB | 5.5MB | 36% |
| 256x256 | 31.6MB | 15.6MB | 51% |
| 512x512 | 118.6MB | 54.8MB | 54% |
| 1024x1024 | 405.8MB | 216.6MB | 47% |
For contours, Arrow reduces Python overhead and still lowers process memory, but the end-to-end savings are smaller than the tracemalloc table alone suggests.
Dtype timing¶
1024x1024 grid, shapes_arrow().
| dtype | time |
|---|---|
| uint8 | 253.0ms |
| uint16 | 255.8ms |
| uint32 | 253.6ms |
| int16 | 253.4ms |
| int32 | 255.2ms |
| float32 | 256.0ms |
| float64 | 254.1ms |
Input dtype has little effect on the Arrow polygonize path in this benchmark; all tested dtypes fall within about 1% of each other.