Skip to content

perf(quickdup): hoist token sets out of the pairwise similarity loop - #13

Merged
rogeralsing merged 1 commit into
mainfrom
perf/quickdup-hoist-token-sets
Jul 25, 2026
Merged

perf(quickdup): hoist token sets out of the pairwise similarity loop#13
rogeralsing merged 1 commit into
mainfrom
perf/quickdup-hoist-token-sets

Conversation

@rogeralsing

Copy link
Copy Markdown
Contributor

Problem

tokenSimilarity took two []string and allocated a map[string]bool for each on every call. clusterBySimilarity calls it once per pattern pair, so every pattern's set was rebuilt n-1 times — 2*O(n²) map allocations per clustering pass.

Found in a 180s CPU profile of a Faktorial daemon: clusterBySimilarity was 29.9% of consumed CPU, and its allocation churn drove most of the 23% spent in runtime.scanobject.

Change

Build each token set once in the tokenize pass that already runs, and pass the prebuilt sets in. Jaccard math and all edge cases unchanged. Comparison now iterates the smaller set, since the result is symmetric.

Measured

Both implementations in one binary (BenchmarkPairLoop), so this is a like-for-like A/B:

old new
n=100 time 109–206 ms 12.7–15.7 ms ~10x
n=100 allocs 56,962 7,861 7.2x fewer
n=100 bytes 14.1 MB 0.55 MB 25x less
n=300 time 565–1327 ms 88–96 ms ~7–13x
n=300 allocs 470,874 23,570 20x fewer
n=300 bytes 125 MB 1.66 MB 75x less

Equivalence

similarity_hoist_test.go pins equivalence rather than leaning on the existing suite:

  • TestTokenSimilarityMatchesPreHoist — exact float equality against a verbatim copy of the old function across 20,000 randomized pairs, plus explicit empty / one-empty / duplicate-heavy / reordered edge cases.
  • TestPairLoopsAgree — pins that the benchmarked A/B pair reaches identical clustering decisions, so the timings compare like with like.

go build, go vet, and the full suite (101 tests) pass on the rebased tree.

🤖 Generated with Claude Code

tokenSimilarity took two []string and built a map[string]bool for each
on every call. clusterBySimilarity calls it once per pattern pair, so
each pattern's set was rebuilt n-1 times: 2*O(n^2) map allocations per
clustering pass.

Build each set once in the tokenize pass that already runs, and pass
the prebuilt sets in. The Jaccard math and every edge case are
unchanged; comparison now iterates the smaller set since the result is
symmetric.

Measured with both implementations in one binary (BenchmarkPairLoop):

  n=100  109-206ms -> 12.7-15.7ms   56,962 -> 7,861 allocs    14.1MB -> 0.55MB
  n=300  565-1327ms -> 88-96ms     470,874 -> 23,570 allocs   125MB -> 1.66MB

This surfaced in a 180s CPU profile of a Faktorial daemon, where
clusterBySimilarity was 29.9% of consumed CPU and its allocation churn
drove most of the 23% spent in GC scanobject.

similarity_hoist_test.go pins equivalence rather than relying on the
existing suite: TestTokenSimilarityMatchesPreHoist checks exact float
equality against a verbatim copy of the old function over 20,000
randomized pairs plus explicit empty/duplicate/reordered edge cases,
and TestPairLoopsAgree pins that the benchmarked A/B pair reaches
identical clustering decisions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 25, 2026 10:30
@rogeralsing
rogeralsing merged commit 469a0dd into main Jul 25, 2026
3 checks passed
@rogeralsing
rogeralsing deleted the perf/quickdup-hoist-token-sets branch July 25, 2026 10:32

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR optimizes QuickDup’s pairwise Jaccard similarity clustering by hoisting per-pattern token set construction out of the O(n²) comparison loop, significantly reducing allocation churn and CPU time during clustering.

Changes:

  • Introduces tokenSet to build per-pattern deduplicated token sets once and reuses them in the clustering pair loop.
  • Updates tokenSimilarity to operate on prebuilt token sets and iterates the smaller set for faster intersection counting.
  • Adds differential tests/benchmarks to pin behavioral equivalence vs the pre-hoist implementation and quantify performance impact.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
pkg/quickdup/similarity.go Hoists token set creation and updates similarity computation to use prebuilt sets in the pairwise loop.
pkg/quickdup/similarity_hoist_test.go Adds pre/post equivalence tests and an A/B benchmark harness for the pairwise loop.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +77 to +83
func tokenSet(tokens []string) map[string]bool {
set := make(map[string]bool, len(tokens))
for _, t := range tokens {
set[t] = true
}
return set
}
Comment on lines 102 to 107
intersection := 0
for t := range setA {
if setB[t] {
for t := range small {
if large[t] {
intersection++
}
}
Comment on lines +145 to +152
func TestPairLoopsAgree(t *testing.T) {
for _, n := range []int{50, 200} {
locs := benchLocations(n)
if got, want := newPairLoop(locs, 0.8), oldPairLoop(locs, 0.8); got != want {
t.Fatalf("n=%d: new loop matched %d pairs, old matched %d", n, got, want)
}
}
}
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.

2 participants