Skip to content

Remove allocations from ALPM, Conan and Gentoo comparators - #42

Merged
andrew merged 2 commits into
git-pkgs:mainfrom
abhinavgautam01:issue-31-alloc-free-ecosystem-comparators
Aug 21, 2026
Merged

Remove allocations from ALPM, Conan and Gentoo comparators#42
andrew merged 2 commits into
git-pkgs:mainfrom
abhinavgautam01:issue-31-alloc-free-ecosystem-comparators

Conversation

@abhinavgautam01

Copy link
Copy Markdown
Contributor

Part of #31. Closes three of the seven checklist boxes: ALPM, Conan and Gentoo/APK.

Approach

Same scanner plus stack-buffer treatment #30 applied to SemVer, RubyGems and NuGet, extended to the three comparators that live in ecosystem_extra.go. Each one now walks both versions in lockstep through string views instead of materializing a component list per side.

Comparator What was allocating Replacement
ALPM splitTypedSegments grew a []typedSegment by append, four times per comparison nextTypedSegment, a lockstep iterator over runs of same-kind bytes
Conan strings.Split plus a recursive parse whose &pre and &build pointers escaped to the heap splitConanVersion returns views; the recursion runs over those views, costing stack frames rather than allocations
Gentoo / APK four strings.Split calls per comparison splitGentooBase, nextGentooSuffix and nextDotPart

Conan's trailing numeric-zero rule, where 1.2.0 and 1.2 are equal, is handled by counting significant components with conanMainLen instead of building a slice and truncating it.

One shared change in schemes.go: nextSemverIdentifier is plain dot-splitting with nothing SemVer-specific in it, so it is renamed nextDotPart and reused by the Gentoo and Conan paths rather than copied. No behavior change, just the two call sites.

Exported APIs and scheme ordering are unchanged.

Results

Measured with Go 1.26 on darwin/arm64. The before column comes from running the identical new benchmark file against main in a throwaway worktree, so the two runs are directly comparable.

Benchmark Before After
ALPM 362.2 ns, 768 B, 10 allocs 88.5 ns, 0 B, 0 allocs
ALPM epoch 84.0 ns, 48 B, 2 allocs 31.2 ns, 0 B, 0 allocs
Conan 665.3 ns, 520 B, 11 allocs 163.2 ns, 0 B, 0 allocs
Conan build 703.2 ns, 672 B, 19 allocs 212.4 ns, 0 B, 0 allocs
Gentoo 173.7 ns, 144 B, 4 allocs 70.9 ns, 0 B, 0 allocs
Gentoo revision 198.3 ns, 160 B, 4 allocs 86.7 ns, 0 B, 0 allocs
APK 172.7 ns, 144 B, 4 allocs 76.1 ns, 0 B, 0 allocs

Between 2.3x and 4.1x faster, with every allocation removed. BenchmarkCompareEcosystem is new in bench_test.go so these paths stay covered, since BenchmarkCompareWithScheme did not reach any of them.

Parity coverage

The issue asks for differential coverage against current behavior, so ecosystem_extra_reference_test.go holds the pre-optimization implementations under ref* names. It was produced by a rename script rather than retyped, so it is a faithful copy of what was on main.

ecosystem_extra_parity_test.go then asserts the rewrite returns an identical result for every ordered pair across two corpora:

  • a 100-entry adversarial set covering separator-only strings, trailing separators, empty components, leading and trailing zeros, trailing letters and stacked suffixes
  • 688 unique version strings harvested from the univers conformance fixtures for alpm, conan, gentoo and apk, which is 473,344 ordered pairs per comparator

A testing.AllocsPerRun test pins the zero-allocation property so a future change cannot quietly reintroduce a heap escape.

Mutation testing

I checked that the parity suite actually bites rather than assuming it. Each of these deliberate breakages produced a failure naming the exact offending input:

Mutation Caught by
Drop Conan trailing-zero trimming compareConan("", "0")
Extract Conan prerelease before build compareConan("1", "1.0+build-alpha")
Treat a trailing _ as having no suffixes compareGentoo("", "_")
Split Gentoo on the last _ instead of the first compareGentoo("1.2.3", "1.2.3_alpha_p1")
Swap ALPM segment-kind precedence compareALPM("1.0", "1a.2b")

One mutation survived, changing the Gentoo no-letter sentinel from -1 to 0. That one is an equivalent mutant rather than a coverage gap: every ASCII letter exceeds both values, so cmpInt returns the same answer either way.

Testing

  • gofmt -l . clean, go build ./... and go vet ./... pass
  • go test -race -count=1 ./... passes on all three packages, conformance suite included
  • golangci-lint run ./... reports 0 issues

@andrew andrew left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The new BenchmarkCompareEcosystem duplicates the harness in BenchmarkCompareWithScheme, so the extended golangci-lint check fails with dupl. Please add the ecosystem cases to the existing benchmark table or extract the shared runner.

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 removes heap allocations from the remaining ecosystem-specific version comparators (ALPM, Conan, Gentoo/APK) by switching from split/slice-based parsing to allocation-free, lockstep scanning over string views, while preserving comparator behavior via differential parity tests.

Changes:

  • Reworks ALPM, Conan, and Gentoo/APK comparison to iterate over runs/components directly (no strings.Split, no slice materialization).
  • Generalizes the shared dot-splitting helper (nextSemverIdentifiernextDotPart) and reuses it in the ecosystem comparators.
  • Adds reference implementations plus parity + zero-allocation tests, and introduces a benchmark to keep these comparators covered by -benchmem.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
schemes.go Renames/generalizes the dot-part iterator helper for reuse outside SemVer.
ecosystem_extra.go Rewrites ALPM/Conan/Gentoo comparison logic to be allocation-free via scanning/string views.
ecosystem_extra_reference_test.go Adds mechanically-copied pre-optimization reference implementations for differential testing.
ecosystem_extra_parity_test.go Adds exhaustive parity checks across corpora + AllocsPerRun tests to pin 0 allocations.
bench_test.go Adds an ecosystem-specific benchmark to keep these comparators visible in perf/alloc regressions.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@abhinavgautam01

Copy link
Copy Markdown
Contributor Author

Thanks, fixed. golangci-lint run --enable dupl ./... reported 2 issues on the previous commit; it now reports 0.

I went with folding the cases into the existing table rather than extracting a runner, so the duplication is removed instead of restructured around. BenchmarkCompareEcosystem is gone and its seven cases now sit in BenchmarkCompareWithScheme with a short comment on why they are there. Net 14 lines fewer. I also dropped the underscores from the sub-benchmark names so they follow the existing Semver and NuGet convention instead of adding a second one.

Since you mentioned the extended check, I also diffed the full linter-set profile of this branch against main to be sure nothing else of mine was hiding in there. No linter category appears on the branch that is absent on main. The only count that moves is nonamedreturns, 6 to 13, which is existing idiom here: main already has it in splitALPMVersion and splitGentooRevision and nextSemverIdentifier used (part, rest string, more bool). I kept that style, so I have left those alone. Happy to change them if you would rather they went.

@andrew
andrew merged commit 0efa100 into git-pkgs:main Aug 21, 2026
2 checks passed
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.

3 participants