Skip to content

Commit a7649ee

Browse files
rsneddsymonds
authored andcommitted
s2: Implement optimized algorthm for EdgeQuery.
The improvment in performance is dramatic: from O(n log n) to ~O(1). Based on initial testing updated the brute force cut-over values for the various min and max distance targets. This is based only on Point Target types. Additional testing is needed for Edge, Cell, CellUnion, and ShapeIndex types to see if they have the same basic cut over or need different choices. The optimized algorithm for EdgeQuery, CellQuery, and PointQuery all use the same priority queue type, so I added a queryQueueEntry type for all three to use. (There is already a type priorityQueue in region coverer). ShapeIndexIterators needed a deep copy to allow creating an independent instance in the same position. Fixes #59. name old time/op new time/op delta EdgeQueryFindEdgesClosestFractal/12 5.65µs ±11% 3.87µs ± 4% ~ (p=0.100 n=3+3) EdgeQueryFindEdgesClosestFractal/48 16.0µs ± 8% 0.9µs ± 9% ~ (p=0.100 n=3+3) EdgeQueryFindEdgesClosestFractal/192 44.7µs ± 3% 0.9µs ± 4% ~ (p=0.100 n=3+3) EdgeQueryFindEdgesClosestFractal/768 158µs ±15% 1µs ± 3% ~ (p=0.100 n=3+3) EdgeQueryFindEdgesClosestFractal/3072 710µs ±22% 1µs ± 4% ~ (p=0.100 n=3+3) EdgeQueryFindEdgesClosestFractal/12288 2.52ms ±12% 0.00ms ±16% ~ (p=0.100 n=3+3) EdgeQueryFindEdgesClosestFractal/49152 10.9ms ±14% 0.0ms ± 8% ~ (p=0.100 n=3+3) name old alloc/op new alloc/op delta EdgeQueryFindEdgesClosestFractal/12 378B ± 1% 379B ± 1% ~ (p=0.500 n=3+3) EdgeQueryFindEdgesClosestFractal/48 748B ± 0% 120B ± 0% ~ (p=0.100 n=3+3) EdgeQueryFindEdgesClosestFractal/192 1.71kB ± 9% 0.12kB ± 0% ~ (p=0.100 n=3+3) EdgeQueryFindEdgesClosestFractal/768 4.42kB ±14% 0.12kB ± 0% ~ (p=0.100 n=3+3) EdgeQueryFindEdgesClosestFractal/3072 15.6kB ± 5% 0.1kB ± 0% ~ (p=0.100 n=3+3) EdgeQueryFindEdgesClosestFractal/12288 41.5kB ±16% 0.1kB ± 0% ~ (p=0.100 n=3+3) EdgeQueryFindEdgesClosestFractal/49152 137kB ±19% 0kB ± 0% ~ (p=0.100 n=3+3) name old allocs/op new allocs/op delta EdgeQueryFindEdgesClosestFractal/12 18.0 ± 0% 18.0 ± 0% ~ (all samples are equal) EdgeQueryFindEdgesClosestFractal/48 33.0 ± 0% 7.0 ± 0% ~ (p=0.100 n=3+3) EdgeQueryFindEdgesClosestFractal/192 73.3 ± 9% 7.0 ± 0% ~ (p=0.100 n=3+3) EdgeQueryFindEdgesClosestFractal/768 181 ±14% 7 ± 0% ~ (p=0.100 n=3+3) EdgeQueryFindEdgesClosestFractal/3072 624 ± 5% 7 ± 0% ~ (p=0.100 n=3+3) EdgeQueryFindEdgesClosestFractal/12288 1.65k ±16% 0.01k ± 0% ~ (p=0.100 n=3+3) EdgeQueryFindEdgesClosestFractal/49152 5.05k ±18% 0.01k ± 0% ~ (p=0.100 n=3+3) Signed-off-by: David Symonds <dsymonds@golang.org>
1 parent a891315 commit a7649ee

10 files changed

+568
-39
lines changed

s2/edge_query.go

Lines changed: 309 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -147,17 +147,11 @@ func (e EdgeQueryResult) IsEmpty() bool {
147147
// Less reports if this results is less that the other first by distance,
148148
// then by (shapeID, edgeID). This is used for sorting.
149149
func (e EdgeQueryResult) Less(other EdgeQueryResult) bool {
150-
if e.distance.less(other.distance) {
151-
return true
150+
if e.distance.chordAngle() != other.distance.chordAngle() {
151+
return e.distance.less(other.distance)
152152
}
153-
if other.distance.less(e.distance) {
154-
return false
155-
}
156-
if e.shapeID < other.shapeID {
157-
return true
158-
}
159-
if other.shapeID < e.shapeID {
160-
return false
153+
if e.shapeID != other.shapeID {
154+
return e.shapeID < other.shapeID
161155
}
162156
return e.edgeID < other.edgeID
163157
}
@@ -219,6 +213,22 @@ type EdgeQuery struct {
219213

220214
// testedEdges tracks the set of shape and edges that have already been tested.
221215
testedEdges map[ShapeEdgeID]uint32
216+
217+
// For the optimized algorihm we precompute the top-level CellIDs that
218+
// will be added to the priority queue. There can be at most 6 of these
219+
// cells. Essentially this is just a covering of the indexed edges, except
220+
// that we also store pointers to the corresponding ShapeIndexCells to
221+
// reduce the number of index seeks required.
222+
indexCovering []CellID
223+
indexCells []*ShapeIndexCell
224+
225+
// The algorithm maintains a priority queue of unprocessed CellIDs, sorted
226+
// in increasing order of distance from the target.
227+
queue *queryQueue
228+
229+
iter *ShapeIndexIterator
230+
maxDistanceCovering []CellID
231+
initialCells []CellID
222232
}
223233

224234
// NewClosestEdgeQuery returns an EdgeQuery that is used for finding the
@@ -245,11 +255,14 @@ func NewClosestEdgeQuery(index *ShapeIndex, opts *EdgeQueryOptions) *EdgeQuery {
245255
if opts == nil {
246256
opts = NewClosestEdgeQueryOptions()
247257
}
248-
return &EdgeQuery{
258+
e := &EdgeQuery{
249259
testedEdges: make(map[ShapeEdgeID]uint32),
250260
index: index,
251261
opts: opts.common,
262+
queue: newQueryQueue(),
252263
}
264+
265+
return e
253266
}
254267

255268
// NewFurthestEdgeQuery returns an EdgeQuery that is used for finding the
@@ -264,11 +277,22 @@ func NewFurthestEdgeQuery(index *ShapeIndex, opts *EdgeQueryOptions) *EdgeQuery
264277
if opts == nil {
265278
opts = NewFurthestEdgeQueryOptions()
266279
}
267-
return &EdgeQuery{
280+
e := &EdgeQuery{
268281
testedEdges: make(map[ShapeEdgeID]uint32),
269282
index: index,
270283
opts: opts.common,
284+
queue: newQueryQueue(),
271285
}
286+
287+
return e
288+
}
289+
290+
// Reset resets the state of this EdgeQuery.
291+
func (e *EdgeQuery) Reset() {
292+
e.indexNumEdges = 0
293+
e.indexNumEdgesLimit = 0
294+
e.indexCovering = nil
295+
e.indexCells = nil
272296
}
273297

274298
// FindEdges returns the edges for the given target that satisfy the current options.
@@ -462,10 +486,7 @@ func (e *EdgeQuery) findEdgesInternal(target distanceTarget, opts *queryOptions)
462486
// If the target takes advantage of maxError then we need to avoid
463487
// duplicate edges explicitly. (Otherwise it happens automatically.)
464488
e.avoidDuplicates = targetUsesMaxError && opts.maxResults > 1
465-
466-
// TODO(roberts): Uncomment when optimized is completed.
467-
e.findEdgesBruteForce()
468-
//e.findEdgesOptimized()
489+
e.findEdgesOptimized()
469490
}
470491
}
471492

@@ -505,8 +526,278 @@ func (e *EdgeQuery) findEdgesBruteForce() {
505526
}
506527
}
507528

529+
func (e *EdgeQuery) findEdgesOptimized() {
530+
e.initQueue()
531+
// Repeatedly find the closest Cell to "target" and either split it into
532+
// its four children or process all of its edges.
533+
for e.queue.size() > 0 {
534+
// We need to copy the top entry before removing it, and we need to
535+
// remove it before adding any new entries to the queue.
536+
entry := e.queue.pop()
537+
538+
if !entry.distance.less(e.distanceLimit) {
539+
e.queue.reset() // Clear any remaining entries.
540+
break
541+
}
542+
// If this is already known to be an index cell, just process it.
543+
if entry.indexCell != nil {
544+
e.processEdges(entry)
545+
continue
546+
}
547+
// Otherwise split the cell into its four children. Before adding a
548+
// child back to the queue, we first check whether it is empty. We do
549+
// this in two seek operations rather than four by seeking to the key
550+
// between children 0 and 1 and to the key between children 2 and 3.
551+
id := entry.id
552+
ch := id.Children()
553+
e.iter.seek(ch[1].RangeMin())
554+
555+
if !e.iter.Done() && e.iter.CellID() <= ch[1].RangeMax() {
556+
e.processOrEnqueueCell(ch[1])
557+
}
558+
if e.iter.Prev() && e.iter.CellID() >= id.RangeMin() {
559+
e.processOrEnqueueCell(ch[0])
560+
}
561+
562+
e.iter.seek(ch[3].RangeMin())
563+
if !e.iter.Done() && e.iter.CellID() <= id.RangeMax() {
564+
e.processOrEnqueueCell(ch[3])
565+
}
566+
if e.iter.Prev() && e.iter.CellID() >= ch[2].RangeMin() {
567+
e.processOrEnqueueCell(ch[2])
568+
}
569+
}
570+
}
571+
572+
func (e *EdgeQuery) processOrEnqueueCell(id CellID) {
573+
if e.iter.CellID() == id {
574+
e.processOrEnqueue(id, e.iter.IndexCell())
575+
} else {
576+
e.processOrEnqueue(id, nil)
577+
}
578+
}
579+
580+
func (e *EdgeQuery) initQueue() {
581+
if len(e.indexCovering) == 0 {
582+
// We delay iterator initialization until now to make queries on very
583+
// small indexes a bit faster (i.e., where brute force is used).
584+
e.iter = NewShapeIndexIterator(e.index)
585+
}
586+
587+
// Optimization: if the user is searching for just the closest edge, and the
588+
// center of the target's bounding cap happens to intersect an index cell,
589+
// then we try to limit the search region to a small disc by first
590+
// processing the edges in that cell. This sets distance_limit_ based on
591+
// the closest edge in that cell, which we can then use to limit the search
592+
// area. This means that the cell containing "target" will be processed
593+
// twice, but in general this is still faster.
594+
//
595+
// TODO(roberts): Even if the cap center is not contained, we could still
596+
// process one or both of the adjacent index cells in CellID order,
597+
// provided that those cells are closer than distanceLimit.
598+
cb := e.target.capBound()
599+
if cb.IsEmpty() {
600+
return // Empty target.
601+
}
602+
603+
if e.opts.maxResults == 1 && e.iter.LocatePoint(cb.Center()) {
604+
e.processEdges(&queryQueueEntry{
605+
distance: e.target.distance().zero(),
606+
id: e.iter.CellID(),
607+
indexCell: e.iter.IndexCell(),
608+
})
609+
// Skip the rest of the algorithm if we found an intersecting edge.
610+
if e.distanceLimit == e.target.distance().zero() {
611+
return
612+
}
613+
}
614+
if len(e.indexCovering) == 0 {
615+
e.initCovering()
616+
}
617+
if e.distanceLimit == e.target.distance().infinity() {
618+
// Start with the precomputed index covering.
619+
for i := range e.indexCovering {
620+
e.processOrEnqueue(e.indexCovering[i], e.indexCells[i])
621+
}
622+
} else {
623+
// Compute a covering of the search disc and intersect it with the
624+
// precomputed index covering.
625+
coverer := &RegionCoverer{MaxCells: 4, LevelMod: 1, MaxLevel: maxLevel}
626+
627+
radius := cb.Radius() + e.distanceLimit.chordAngleBound().Angle()
628+
searchCB := CapFromCenterAngle(cb.Center(), radius)
629+
maxDistCover := coverer.FastCovering(searchCB)
630+
e.initialCells = CellUnionFromIntersection(e.indexCovering, maxDistCover)
631+
632+
// Now we need to clean up the initial cells to ensure that they all
633+
// contain at least one cell of the ShapeIndex. (Some may not intersect
634+
// the index at all, while other may be descendants of an index cell.)
635+
i, j := 0, 0
636+
for i < len(e.initialCells) {
637+
idI := e.initialCells[i]
638+
// Find the top-level cell that contains this initial cell.
639+
for e.indexCovering[j].RangeMax() < idI {
640+
j++
641+
}
642+
643+
idJ := e.indexCovering[j]
644+
if idI == idJ {
645+
// This initial cell is one of the top-level cells. Use the
646+
// precomputed ShapeIndexCell pointer to avoid an index seek.
647+
e.processOrEnqueue(idJ, e.indexCells[j])
648+
i++
649+
j++
650+
} else {
651+
// This initial cell is a proper descendant of a top-level cell.
652+
// Check how it is related to the cells of the ShapeIndex.
653+
r := e.iter.LocateCellID(idI)
654+
if r == Indexed {
655+
// This cell is a descendant of an index cell.
656+
// Enqueue it and skip any other initial cells
657+
// that are also descendants of this cell.
658+
e.processOrEnqueue(e.iter.CellID(), e.iter.IndexCell())
659+
lastID := e.iter.CellID().RangeMax()
660+
for i < len(e.initialCells) && e.initialCells[i] <= lastID {
661+
i++
662+
}
663+
} else {
664+
// Enqueue the cell only if it contains at least one index cell.
665+
if r == Subdivided {
666+
e.processOrEnqueue(idI, nil)
667+
}
668+
i++
669+
}
670+
}
671+
}
672+
}
673+
}
674+
675+
func (e *EdgeQuery) initCovering() {
676+
// Find the range of Cells spanned by the index and choose a level such
677+
// that the entire index can be covered with just a few cells. These are
678+
// the "top-level" cells. There are two cases:
679+
//
680+
// - If the index spans more than one face, then there is one top-level cell
681+
// per spanned face, just big enough to cover the index cells on that face.
682+
//
683+
// - If the index spans only one face, then we find the smallest cell "C"
684+
// that covers the index cells on that face (just like the case above).
685+
// Then for each of the 4 children of "C", if the child contains any index
686+
// cells then we create a top-level cell that is big enough to just fit
687+
// those index cells (i.e., shrinking the child as much as possible to fit
688+
// its contents). This essentially replicates what would happen if we
689+
// started with "C" as the top-level cell, since "C" would immediately be
690+
// split, except that we take the time to prune the children further since
691+
// this will save work on every subsequent query.
692+
e.indexCovering = make([]CellID, 0, 6)
693+
694+
// TODO(roberts): Use a single iterator below and save position
695+
// information using pair {CellID, ShapeIndexCell}.
696+
next := NewShapeIndexIterator(e.index, IteratorBegin)
697+
last := NewShapeIndexIterator(e.index, IteratorEnd)
698+
last.Prev()
699+
if next.CellID() != last.CellID() {
700+
// The index has at least two cells. Choose a level such that the entire
701+
// index can be spanned with at most 6 cells (if the index spans multiple
702+
// faces) or 4 cells (it the index spans a single face).
703+
level, ok := next.CellID().CommonAncestorLevel(last.CellID())
704+
if !ok {
705+
level = 0
706+
} else {
707+
level++
708+
}
709+
710+
// Visit each potential top-level cell except the last (handled below).
711+
lastID := last.CellID().Parent(level)
712+
for id := next.CellID().Parent(level); id != lastID; id = id.Next() {
713+
// Skip any top-level cells that don't contain any index cells.
714+
if id.RangeMax() < next.CellID() {
715+
continue
716+
}
717+
718+
// Find the range of index cells contained by this top-level cell and
719+
// then shrink the cell if necessary so that it just covers them.
720+
cellFirst := next.clone()
721+
next.seek(id.RangeMax().Next())
722+
cellLast := next.clone()
723+
cellLast.Prev()
724+
e.addInitialRange(cellFirst, cellLast)
725+
break
726+
}
727+
728+
}
729+
e.addInitialRange(next, last)
730+
}
731+
732+
// addInitialRange adds an entry to the indexCovering and indexCells that covers the given
733+
// inclusive range of cells.
734+
//
735+
// This requires that first and last cells have a common ancestor.
736+
func (e *EdgeQuery) addInitialRange(first, last *ShapeIndexIterator) {
737+
if first.CellID() == last.CellID() {
738+
// The range consists of a single index cell.
739+
e.indexCovering = append(e.indexCovering, first.CellID())
740+
e.indexCells = append(e.indexCells, first.IndexCell())
741+
} else {
742+
// Add the lowest common ancestor of the given range.
743+
level, _ := first.CellID().CommonAncestorLevel(last.CellID())
744+
e.indexCovering = append(e.indexCovering, first.CellID().Parent(level))
745+
e.indexCells = append(e.indexCells, nil)
746+
}
747+
}
748+
749+
// processEdges processes all the edges of the given index cell.
750+
func (e *EdgeQuery) processEdges(entry *queryQueueEntry) {
751+
for _, clipped := range entry.indexCell.shapes {
752+
shape := e.index.Shape(clipped.shapeID)
753+
for j := 0; j < clipped.numEdges(); j++ {
754+
e.maybeAddResult(shape, int32(clipped.edges[j]))
755+
}
756+
}
757+
}
758+
759+
// processOrEnqueue the given cell id and indexCell.
760+
func (e *EdgeQuery) processOrEnqueue(id CellID, indexCell *ShapeIndexCell) {
761+
if indexCell != nil {
762+
// If this index cell has only a few edges, then it is faster to check
763+
// them directly rather than computing the minimum distance to the Cell
764+
// and inserting it into the queue.
765+
const minEdgesToEnqueue = 10
766+
numEdges := indexCell.numEdges()
767+
if numEdges == 0 {
768+
return
769+
}
770+
if numEdges < minEdgesToEnqueue {
771+
// Set "distance" to zero to avoid the expense of computing it.
772+
e.processEdges(&queryQueueEntry{
773+
distance: e.target.distance().zero(),
774+
id: id,
775+
indexCell: indexCell,
776+
})
777+
return
778+
}
779+
}
780+
781+
// Otherwise compute the minimum distance to any point in the cell and add
782+
// it to the priority queue.
783+
cell := CellFromCellID(id)
784+
dist := e.distanceLimit
785+
var ok bool
786+
if dist, ok = e.target.updateDistanceToCell(cell, dist); !ok {
787+
return
788+
}
789+
if e.useConservativeCellDistance {
790+
// Ensure that "distance" is a lower bound on the true distance to the cell.
791+
dist = dist.sub(e.target.distance().fromChordAngle(e.opts.maxError))
792+
}
793+
794+
e.queue.push(&queryQueueEntry{
795+
distance: dist,
796+
id: id,
797+
indexCell: indexCell,
798+
})
799+
}
800+
508801
// TODO(roberts): Remaining pieces
509-
// Add clear/reset/re-init method to empty out the state of the query.
510-
// findEdgesOptimized and related methods.
511802
// GetEdge
512803
// Project

0 commit comments

Comments
 (0)