Skip to content

RelateNG prepared mode rebuilds the B-side geometry every call: ~35 kB/call in a one-target x many-geometries sweep #469

Description

@asinghvi17

prepare(RelateNG(), a) caches the A side and relate_predicate(prep, pred, b) reuses it — that part works, and it is what we use downstream. But evaluate! builds a fresh RelateGeometry for b on every call:

function evaluate!(alg::RelateNG, geom_a::RelateGeometry, b, predicate::TopologyPredicate,
prep = nothing)
#-- Java performs the envelope fast-exit before building the B
#-- RelateGeometry, reading the envelope cached on the Geometry. Here the
#-- RelateGeometry constructor is what caches the extents (one coordinate
#-- pass either way), so B is built first and the check reads its extent.
geom_b = RelateGeometry(geom_a.m, b; exact = geom_a.exact,
boundary_rule = geom_a.boundary_rule)
if !has_required_envelope_interaction(geom_a, get_extent(geom_b), predicate)

and there is no hook to reuse that work or its buffers across calls. In the one-target-versus-many-geometries sweep that prepared mode exists to serve, the B side is then the entire remaining cost.

Measured on main @ 4e0c0d5 (Apple M-series, Julia 1.11), one prepared Spherical() polygon target, pred_contains (the converse of a Within query). Full script in a comment below.

Per-call cost is flat in the target's size — prepared mode is doing its job:

target vertices 4 32 72 312 616 736
KiB/call 7.5 7.5 7.5 7.5 7.5 7.5
us/call 8.35 8.38 8.54 8.19 8.30 8.00

and linear in the query geometry's size, at ~1 KiB and ~0.8 us per vertex:

query vertices 3 6 16 32 64 128
KiB/call 4.5 7.5 17.7 33.8 66.1 132.0
us/call 6.23 7.86 15.29 27.79 55.50 102.75

For scale, prepare on those same targets costs 7.3 KiB / 13 us (4 verts) up to 899.5 KiB / 645 us (736 verts) — so against a large target a single 128-vertex query call is already ~15% of a full re-preparation, and against a small one it is several times the preparation.

A point query is the one cheap case, at 0.1 KiB / 0.01 us. That is a query-shape short-circuit, not a predicate one: pred_contains, pred_within and pred_intersects on the same 6-vertex ring all cost an identical 7.5 KiB.

Access pattern: one target × thousands of small cells. In DiscreteGlobalGrids' polygon-coverage traversal the same prepared outline is tested against every candidate cell of a DGGS level. Measured there, the predicate is 79% of wall time at H3 L7 and 81% at L9, and 90% of allocations (3.26 GiB total at L9). Removing the per-call rebuild is worth a measured ~3.7-4.2x end-to-end and ~6.6x memory, which would put a generic spherical DFS ahead of libh3's specialised polygonToCells + compactCells at every level tested. It is expensive enough today that we deliberately carry a cheap provenance-only "contained" label rather than ask the predicate per boundary leaf.

The B geometries genuinely differ call to call, so rebuilding something is unavoidable — but in this pattern they are all rings of the same small, fixed vertex count, so it is per-call scratch churn rather than retained state. A workspace/buffer argument (relate_predicate(prep, pred, b; workspace)) or an in-place RelateGeometry!(scratch, m, b; …) would cover it.

Separately: the clipping algorithms accept no prepared geometry at all — intersection(ConvexConvexSutherlandHodgman, a, b) re-walks both rings every call, with the same one-destination × many-sources access pattern in ConservativeRegridding.

Tested revs: measured on main @ 4e0c0d5; same behaviour on as/dualdfs-carry-extents @ a658e8b.

(#87 asked for prepared geometry in general and is closed — prepare landed. This is the residual half: the unprepared side of a prepared query.) Related: #418, #408, #23.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions