Repository navigation
Conversation
The traversal queues regrew from nil on every path insertion, which accounted for ~90% of allocated bytes. BenchmarkConflictTree/pkgs=1000: -91% B/op, -37% allocs/op, -17% sec/op.
A literal segment scanned every sibling node linearly even though Children is keyed by segment text. Look up the exact child directly and scan only wildcard children, tracked in a new GlobChildren list. BenchmarkConflictTree/pkgs=1000: -70% sec/op; DeepPaths: -85% sec/op.
| type pathConflictTree struct { | ||
| Root *node | ||
| PathToSlices map[string][]*Slice | ||
| // currentQueue and nextQueue are kept across pathHasConflict calls so |
There was a problem hiding this comment.
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.
| currentQueue := g.currentQueue[:0] | ||
| nextQueue := g.nextQueue[:0] |
There was a problem hiding this comment.
Even though we now have state we are clearing it before each run which makes it safe.
There was a problem hiding this comment.
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 pathHasConflict. What about also clearing the queues in the deferred call?
| Segment segment | ||
| SegmentSlices []*segmentSlice | ||
| Children map[string]*node | ||
| // GlobChildren lists the children whose segment contains a wildcard, so |
There was a problem hiding this comment.
This also looks okay to me. There are many optimizations optimizations we can do when looking for Children to reduce the amount of comparisons. GlobChildren seems 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...
upils
left a comment
There was a problem hiding this comment.
Thanks @lczyk. This is nice to gain even more perf with a reasonable added complexity.
Once the suggested changes are done I think I will still keep this PR in Group Review as I would like to keep the reviews focused on other more pressing matters for now.
| currentQueue := g.currentQueue[:0] | ||
| nextQueue := g.nextQueue[:0] | ||
| defer func() { | ||
| // Keep the grown queues for the next call. |
There was a problem hiding this comment.
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.
| for _, child := range parent.Children { | ||
| queue = append(queue, child) | ||
| } | ||
| return queue |
There was a problem hiding this comment.
| for _, child := range parent.Children { | |
| queue = append(queue, child) | |
| } | |
| return queue | |
| return slices.AppendSeq(queue, maps.Values(parent.Children)) |
| currentQueue := g.currentQueue[:0] | ||
| nextQueue := g.nextQueue[:0] |
There was a problem hiding this comment.
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 pathHasConflict. What about also clearing the queues in the deferred call?
since #302 got merged, here is a followup which was originally designed as letFunny#29
two optimisations for
pathConflictTree, on top of the trie from this branch. no behaviour change intended: same conflicts detected, same error type. tests pass unchanged. checked with my fuzz tests too.1. reuse the traversal queues (
f449d7a)pathHasConflictused to growcurrentQueueandnextQueuefrom nil on every path insertion, and seeded the first level withslices.Collect(maps.Values(...)). with a few thousand paths this dominated allocation: the queues were the single largest source of gc work in the whole conflict check.i've changed
currentQueueandnextQueueto fields onpathConflictTree, reset with[:0]per call and restored through adefer, so the backing arrays are grown once and reused.proof of gc work (drop somewhere in
internal/setup):after:
2. exact-match child lookup (
3c097a7)when a segment matched, the old traversal enqueued every child of the matched node and compared each one against the next segment on the following round. but
Childrenis already keyed by segment text, so for a literal next-segment the only children that can possibly match are the one with the identical text and any child whose segment holds a wildcard.now
appendCandidatesdoes and exact map lookup, plus a scan of the newnode.GlobChildrenlist. a wildcard next-segment still considers every child, since it can match anything. glob-vs-glob comparisons are unchanged.this turns the linear sibling scan in wide directories into a map lookup. real chisel-releases paths are at present aboout ~60% literal directories and this speeds this up, especially because the wide nodes tend to also be mostly literal so the quadratic scan hurts a lot there).
proof, with fix 1 above applied, otherwise GC noise swamps the signal:
after: