Skip to content

perf: replace sort-based epssPercentile with O(n) linear scan#3316

Open
matiasinsaurralde wants to merge 1 commit intoanchore:mainfrom
matiasinsaurralde:perf/epss-percentile-linear-scan
Open

perf: replace sort-based epssPercentile with O(n) linear scan#3316
matiasinsaurralde wants to merge 1 commit intoanchore:mainfrom
matiasinsaurralde:perf/epss-percentile-linear-scan

Conversation

@matiasinsaurralde
Copy link
Copy Markdown
Contributor

Summary

epssPercentile was calling sort.Slice to find the maximum value in a slice — O(n log n) with 4 heap allocations — when a single linear pass suffices.

This replaces it with an O(n), zero-allocation loop.

Bonus fix: sort.Slice sorted the caller's slice in-place, silently mutating a.Vulnerability.EPSS during output rendering as a sideeffect. The linear scan is pure (read-only).

Benchmarks (Apple M4 Pro)

Input Before After Speedup Allocs before Allocs after
3 items 100.6 ns/op 2.2 ns/op 45x 4 0
100 items 1131 ns/op 62 ns/op 18x 4 0

In practice, EPSS slices are length 0–1 per CVE (both implementations are already O(1) there), so the primary value of this change is the removal of the mutation side-effect and the allocation savings on any multi-entry path

Test plan

  • Existing TestEPSSPercentile unit tests pass unchanged
  • All tests in grype/presenter/models pass
  • Benchmark file added: epss_bench_test.go

Feel free to leave the benchmark out from the PR if it's not relevant

Finding max of a slice doesn't require sorting. The previous
implementation called sort.Slice (O(n log n), 4 heap allocations) on
every sort comparison. The linear scan is O(n) with zero allocations.

It also removes a subtle mutation side-effect: sort.Slice was sorting
the caller's EPSS slice in-place, silently modifying the match struct
during output rendering.

In practice EPSS slices are almost always length 0–1 (both paths remain
O(1) for those), so the main value of this change is correctness
(no mutation) and the allocation reduction on the multi-entry path.

Benchmark (Apple M4):
  small (3 items):  100.6 ns/op → 2.2 ns/op  (45x), 4 allocs → 0
  large (100 items): 1131 ns/op → 62 ns/op   (18x), 4 allocs → 0

Signed-off-by: Matías Insaurralde <matias@insaurral.de>
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.

1 participant