Repository navigation
perf: even faster trie #327
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -32,6 +32,10 @@ type node struct { | |||||||||||
| Segment segment | ||||||||||||
| SegmentSlices []*segmentSlice | ||||||||||||
| Children map[string]*node | ||||||||||||
| // GlobChildren lists the children whose segment contains a wildcard, so | ||||||||||||
| // that a literal segment can find its exact match in Children and only | ||||||||||||
| // scan these. | ||||||||||||
| GlobChildren []*node | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // pathConflictTree uses a custom trie to find conflicts that might arise from | ||||||||||||
|
|
@@ -52,6 +56,10 @@ type node struct { | |||||||||||
| type pathConflictTree struct { | ||||||||||||
| Root *node | ||||||||||||
| PathToSlices map[string][]*Slice | ||||||||||||
| // currentQueue and nextQueue are kept across pathHasConflict calls so | ||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is okay, the downside is that we are adding state and keeping the queues in memory for as long as the object lives. I think it is a good tradeoff as the graph will also be in memory for the same amount of time. |
||||||||||||
| // that the traversal does not have to grow fresh queues for every path. | ||||||||||||
| currentQueue []*node | ||||||||||||
| nextQueue []*node | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| var rootSegment = segment{"/", false, false} | ||||||||||||
|
|
@@ -100,12 +108,19 @@ func (g *pathConflictTree) pathHasConflict(newSegments []segment, newSegmentSlic | |||||||||||
| return fmt.Errorf("slices %s and %s conflict on %s and %s", oldSlice, newSlice, oldPath, newPath) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| var currentQueue []*node | ||||||||||||
| var nextQueue []*node | ||||||||||||
| currentQueue := g.currentQueue[:0] | ||||||||||||
| nextQueue := g.nextQueue[:0] | ||||||||||||
|
Comment on lines
+111
to
+112
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Even though we now have state we are clearing it before each run which makes it safe.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: With the current approach both queues are non-empty. So one could think that the stored values are meaningful and may be used by another function/method between executions of |
||||||||||||
| defer func() { | ||||||||||||
| // Keep the grown queues for the next call. | ||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe it should be very clear that these are kept only to avoid more allocations, and it does not serve any other purpose in the algorithm. So far the recently added complexity was essentially due to implementing the logic itself. The propose change here is a pure optimization and being able to "ignore" it when reasoning about the logic might help. |
||||||||||||
| g.currentQueue, g.nextQueue = currentQueue, nextQueue | ||||||||||||
| }() | ||||||||||||
|
|
||||||||||||
| // Skip "/". | ||||||||||||
| currentQueue = slices.Collect(maps.Values(g.Root.Children)) | ||||||||||||
| newSegments = newSegments[1:] | ||||||||||||
| if len(newSegments) == 0 { | ||||||||||||
| return nil | ||||||||||||
| } | ||||||||||||
| currentQueue = appendCandidates(currentQueue, g.Root, newSegments[0]) | ||||||||||||
|
|
||||||||||||
| // If we run out of segments from the graph or the path there cannot be a | ||||||||||||
| // conflict. | ||||||||||||
|
|
@@ -152,8 +167,8 @@ func (g *pathConflictTree) pathHasConflict(newSegments []segment, newSegmentSlic | |||||||||||
| // If we are at the terminal node of both paths we found a conflict. | ||||||||||||
| return conflictErrMsg(oldSegmentSlice, newSegmentSlice) | ||||||||||||
| } | ||||||||||||
| for _, child := range oldNode.Children { | ||||||||||||
| nextQueue = append(nextQueue, child) | ||||||||||||
| if len(newSegments) > 1 { | ||||||||||||
| nextQueue = appendCandidates(nextQueue, oldNode, newSegments[1]) | ||||||||||||
| } | ||||||||||||
| break oldNodeLoop | ||||||||||||
| } else { | ||||||||||||
|
|
@@ -174,6 +189,23 @@ func (g *pathConflictTree) pathHasConflict(newSegments []segment, newSegmentSlic | |||||||||||
| return nil | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // appendCandidates appends the children of parent that can possibly match | ||||||||||||
| // seg. A segment with a wildcard can match any child, but a literal segment | ||||||||||||
| // can only match the child with the exact same text or children with | ||||||||||||
| // wildcards. | ||||||||||||
| func appendCandidates(queue []*node, parent *node, seg segment) []*node { | ||||||||||||
| if seg.HasGlob { | ||||||||||||
| for _, child := range parent.Children { | ||||||||||||
| queue = append(queue, child) | ||||||||||||
| } | ||||||||||||
| return queue | ||||||||||||
|
Comment on lines
+198
to
+201
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
| } | ||||||||||||
| if child, ok := parent.Children[seg.Text]; ok { | ||||||||||||
| queue = append(queue, child) | ||||||||||||
| } | ||||||||||||
| return append(queue, parent.GlobChildren...) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // insertSegments inserts the path's segments blindly in the graph without | ||||||||||||
| // looking at conflicts. | ||||||||||||
| func (g *pathConflictTree) insertSegments(segments []segment, segmentSlices []*segmentSlice) { | ||||||||||||
|
|
@@ -188,9 +220,12 @@ func (g *pathConflictTree) insertSegments(segments []segment, segmentSlices []*s | |||||||||||
| Segment: segment, | ||||||||||||
| Children: map[string]*node{}, | ||||||||||||
| } | ||||||||||||
| parent.Children[segment.Text] = current | ||||||||||||
| if segment.HasGlob { | ||||||||||||
| parent.GlobChildren = append(parent.GlobChildren, current) | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| current.SegmentSlices = append(current.SegmentSlices, segmentSlices...) | ||||||||||||
| parent.Children[segment.Text] = current | ||||||||||||
| parent = current | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This also looks okay to me. There are many optimizations optimizations we can do when looking for Children to reduce the amount of comparisons.
GlobChildrenseems a good compromise between being quite simple and having an impact on performance. On that note, what is the effect? I remember you said ~20%.Nit: I find the comment describes the usage with regard to the other field in a bit of a convoluted way. I would instead say this is an optimization that has the subset of children with globs and it is used in
appendCandidates...