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.
prepare(RelateNG(), a)caches the A side andrelate_predicate(prep, pred, b)reuses it — that part works, and it is what we use downstream. Butevaluate!builds a freshRelateGeometryforbon every call:GeometryOps.jl/src/methods/geom_relations/relateng/relate_ng.jl
Lines 272 to 280 in 4e0c0d5
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 preparedSpherical()polygon target,pred_contains(the converse of aWithinquery). Full script in a comment below.Per-call cost is flat in the target's size — prepared mode is doing its job:
and linear in the query geometry's size, at ~1 KiB and ~0.8 us per vertex:
For scale,
prepareon 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_withinandpred_intersectson 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+compactCellsat 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-placeRelateGeometry!(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 onas/dualdfs-carry-extents@ a658e8b.(#87 asked for prepared geometry in general and is closed —
preparelanded. This is the residual half: the unprepared side of a prepared query.) Related: #418, #408, #23.