Skip to content

geom: build nested GeometryCollections in linear time - #717

Merged
peterstace merged 1 commit into
peterstace:masterfrom
mjonss:nested_collection_copy
Nov 11, 2026
Merged

peterstace merged 1 commit into
peterstace:masterfrom
mjonss:nested_collection_copy

Conversation

@mjonss

@mjonss mjonss commented Nov 10, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes #716 "Parsing nested GeometryCollections is quadratic in the nesting
depth".

NewGeometryCollection forced every member to the collection's coordinates
type, and GeometryCollection.ForceCoordinatesType rebuilt all of its members
recursively even when the coordinates type was already the requested one. The
parsers build nested collections bottom up, one NewGeometryCollection call
per level, so a collection nested N levels deep had its innermost members
copied N times.

This change:

  • Only forces members whose coordinates type differs from the collection's.
  • Returns the collection as is from ForceCoordinatesType when it already has
    the requested coordinates type.

Results are unchanged (apart from the reflect.DeepEqual detail for empty
collections noted in #716). Members that need a different coordinates type are
still forced, including inside nested collections. Parsing non-collection
geometries is unaffected, and flat collections get slightly cheaper since
members are no longer re-wrapped.

I used Claude Code to prepare this change.

Check List

Have you:

  • Added unit tests? Yes, in the new geom/type_geometry_collection_test.go:

    • TestNewGeometryCollectionDoesNotCopyNestedMembers: wrapping a 100 deep
      collection allocates no more than wrapping a 1 deep one (204 vs 6
      allocations before this change).
    • TestGeometryCollectionForceCoordinatesTypeUnchanged: forcing to the
      current coordinates type allocates nothing and returns an equal value (201
      allocations before this change).
    • TestNewGeometryCollectionForcesNestedMembers: a nested POINT Z is
      still forced to XY when combined with an XY point (passes before and
      after).
  • Add cmprefimpl tests? Not appropriate, results are unchanged.

  • Updated release notes? Yes, an entry in the Unreleased section of
    CHANGELOG.md.

  • Updated the README.md? Not applicable, no new functionality.

Related Issue

Benchmark Results

  • Added BenchmarkUnmarshalWKB/nested_collection, which parses a 1,000 point
    MultiPoint wrapped in 1, 10 and 100 levels of GeometryCollection. Below
    is benchstat over 8 interleaved runs of BenchmarkUnmarshalWKB and
    BenchmarkWKTParsing (run with go test -bench rather than
    run_benchmarks.sh).
Click to expand
                                         │     before     │                after                │
                                         │     sec/op     │    sec/op     vs base               │
UnmarshalWKB/polygon/n=10                    93.48n ± 10%   92.60n ±  1%        ~ (p=0.138 n=8)
UnmarshalWKB/polygon/n=100                   221.2n ±  9%   220.8n ±  2%        ~ (p=0.279 n=8)
UnmarshalWKB/polygon/n=1000                  1.452µ ±  7%   1.451µ ±  2%        ~ (p=0.666 n=8)
UnmarshalWKB/polygon/n=10000                 10.95µ ±  3%   10.79µ ±  1%        ~ (p=0.105 n=8)
UnmarshalWKB/nested_collection/depth=1       48.31µ ±  1%   40.97µ ±  2%  -15.19% (p=0.000 n=8)
UnmarshalWKB/nested_collection/depth=10     127.43µ ±  2%   50.74µ ± 13%  -60.18% (p=0.000 n=8)
UnmarshalWKB/nested_collection/depth=100   1056.33µ ±  6%   51.16µ ±  1%  -95.16% (p=0.000 n=8)
WKTParsing/point                             820.1n ±  1%   815.2n ±  1%        ~ (p=0.083 n=8)
geomean                                      6.032µ         3.593µ        -40.44%

                                         │    before     │                 after                 │
                                         │     B/op      │     B/op      vs base                 │
UnmarshalWKB/polygon/n=10                     272.0 ± 0%     272.0 ± 0%        ~ (p=1.000 n=8) ¹
UnmarshalWKB/polygon/n=100                  1.844Ki ± 0%   1.844Ki ± 0%        ~ (p=1.000 n=8) ¹
UnmarshalWKB/polygon/n=1000                 16.09Ki ± 0%   16.09Ki ± 0%        ~ (p=1.000 n=8) ¹
UnmarshalWKB/polygon/n=10000                160.1Ki ± 0%   160.1Ki ± 0%        ~ (p=1.000 n=8)
UnmarshalWKB/nested_collection/depth=1      191.0Ki ± 0%   143.0Ki ± 0%  -25.15% (p=0.000 n=8)
UnmarshalWKB/nested_collection/depth=10     626.0Ki ± 0%   143.5Ki ± 0%  -77.07% (p=0.000 n=8)
UnmarshalWKB/nested_collection/depth=100   5184.3Ki ± 0%   149.2Ki ± 0%  -97.12% (p=0.000 n=8)
WKTParsing/point                            1.731Ki ± 0%   1.731Ki ± 0%        ~ (p=1.000 n=8) ¹
geomean                                     32.84Ki        16.91Ki       -48.51%
¹ all samples are equal

                                         │    before    │                after                 │
                                         │  allocs/op   │  allocs/op   vs base                 │
UnmarshalWKB/polygon/n=10                    4.000 ± 0%    4.000 ± 0%        ~ (p=1.000 n=8) ¹
UnmarshalWKB/polygon/n=100                   4.000 ± 0%    4.000 ± 0%        ~ (p=1.000 n=8) ¹
UnmarshalWKB/polygon/n=1000                  4.000 ± 0%    4.000 ± 0%        ~ (p=1.000 n=8) ¹
UnmarshalWKB/polygon/n=10000                 4.000 ± 0%    4.000 ± 0%        ~ (p=1.000 n=8) ¹
UnmarshalWKB/nested_collection/depth=1      1.008k ± 0%   1.006k ± 0%   -0.20% (p=0.000 n=8)
UnmarshalWKB/nested_collection/depth=10     1.143k ± 0%   1.033k ± 0%   -9.62% (p=0.000 n=8)
UnmarshalWKB/nested_collection/depth=100   11.403k ± 0%   1.303k ± 0%  -88.57% (p=0.000 n=8)
WKTParsing/point                             23.00 ± 0%    23.00 ± 0%        ~ (p=1.000 n=8) ¹
geomean                                      54.46         40.99       -24.73%
¹ all samples are equal

NewGeometryCollection forced every member to the collection's coordinates
type, and GeometryCollection.ForceCoordinatesType rebuilt every member
recursively even when the type was unchanged. Building a collection N
levels deep, as the parsers do one level at a time, therefore copied the
innermost members N times.

Only force members whose coordinates type differs, and return the
collection as is from ForceCoordinatesType when nothing changes.

Add BenchmarkUnmarshalWKB/nested_collection.
@peterstace

Copy link
Copy Markdown
Owner

Thanks for the change!

@peterstace
peterstace merged commit ce2170a into peterstace:master Nov 11, 2026
1 check passed
@mjonss
mjonss deleted the nested_collection_copy branch November 11, 2026 08:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Parsing nested GeometryCollections is quadratic in the nesting depth

2 participants