From c2a0fcf8a01b409c4573634940160bf22bf740c4 Mon Sep 17 00:00:00 2001 From: "daniel.korn" Date: Sun, 1 Nov 2026 16:42:53 +0100 Subject: [PATCH] perf(rect_clip): skip segments beyond one edge, don't load large polygons twice #949 made profiles with many huge polygons 52% slower: rect_clip ran the Liang-Barsky computation for every segment of the whole polygon on every tile, and the polygon was loaded from the store a second time because fast_clip had modified the first copy. - clipRing rejects a segment whose end points both lie beyond the same box edge with four comparisons, before the clip computation - a valid polygon over 1,000 points is kept for rect_clip; fast_clip works on a copy instead of the moved original parks profile, Alaska/BC/Yukon, 16 threads: 275.6 -> 210.2 s (-24%), CPU -26%. Output identical (parks extract, Germany OpenMapTiles). Co-Authored-By: Claude Opus 5.5 (1M context) --- src/geom.cpp | 9 ++++++++- src/tile_data.cpp | 12 +++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/geom.cpp b/src/geom.cpp index f7e1da93..f060dc96 100644 --- a/src/geom.cpp +++ b/src/geom.cpp @@ -762,6 +762,11 @@ struct RectClipper { bool inside(const Point &p) const { return p.x() >= x0 && p.x() <= x1 && p.y() >= y0 && p.y() <= y1; } + // Both end points beyond the same edge: the segment can't reach the box + bool beyondOneEdge(const Point &a, const Point &b) const { + return (a.x() < x0 && b.x() < x0) || (a.x() > x1 && b.x() > x1) || + (a.y() < y0 && b.y() < y0) || (a.y() > y1 && b.y() > y1); + } bool strictlyInside(const Point &p) const { return p.x() > x0 && p.x() < x1 && p.y() > y0 && p.y() < y1; } @@ -888,7 +893,9 @@ RingClip clipRing(const RectClipper &rc, const Ring &ring, std::vector t0; + // Most segments of a large polygon lie far from a small tile's box: reject + // those with four comparisons before the clip computation + const bool hit = !rc.beyondOneEdge(a, b) && rc.clipSegment(a, b, t0, t1, pa, pb) && t1 > t0; if (!hit || (building && t0 > 0)) { if (building && !finish()) return RingClip::Fail; if (!hit) continue; diff --git a/src/tile_data.cpp b/src/tile_data.cpp index 89b367e0..096ae43a 100644 --- a/src/tile_data.cpp +++ b/src/tile_data.cpp @@ -385,7 +385,13 @@ Geometry TileDataSource::buildWayGeometry(OutputGeometryType const geomType, } MultiPolygon mp; - if (cachedClip == nullptr) + // rect_clip below needs the input as it was before fast_clip. For a large valid + // polygon, keep it rather than load it from the store again for every tile. + std::size_t inputPoints = 0; + if (cachedClip == nullptr && inputKnownValid) + for (auto const &p : uncached) inputPoints += geom::num_points(p); + const bool keepUncached = inputPoints > 1000; + if (cachedClip == nullptr && !keepUncached) mp = std::move(uncached); else geom::assign(mp, input); @@ -405,9 +411,9 @@ Geometry TileDataSource::buildWayGeometry(OutputGeometryType const geomType, // instead, which is much cheaper than the is_valid/intersection/repair // path below; rect_clip bails out on anything it can't handle. MultiPolygon original; - if (cachedClip == nullptr) populateMultiPolygon(original, objectID); + if (cachedClip == nullptr && !keepUncached) populateMultiPolygon(original, objectID); MultiPolygon clipped; - if (rect_clip(cachedClip == nullptr ? original : input, box, clipped) && + if (rect_clip(cachedClip == nullptr && !keepUncached ? original : input, box, clipped) && (multiPolygonDefinitelyValid(clipped) || geom::is_valid(clipped))) { multiPolygonClipCache.add(bbox, objectID, clipped, true); return clipped;