Skip to content

Conversation

@abhishek-singla-97
Copy link

@abhishek-singla-97 abhishek-singla-97 commented Dec 27, 2025

Summary

This PR optimizes the consistent hash implementation by replacing the separate keyHashes []uint32 slice and hashMap map[uint32]string with a single segments []segment struct slice. This eliminates the map lookup overhead in the hot path (Get(), findSegmentOwner()).

Changes

  • Added segment struct combining hash uint32 and owner string
  • Replaced keyHashes and hashMap with segments []segment
  • Updated findSegmentOwner() to access segments directly (the key optimization)
  • Updated Add() to use a temporary map during insertion for collision handling
  • Updated tests to work with the new structure

Benchmark Comparison

goos: darwin
goarch: arm64
cpu: Apple M1 Pro
Benchmark Old (ns/op) New (ns/op) Speedup
Get/segs50-shards8 37.99 34.47 9.3% faster
Get/segs50-shards32 40.66 37.41 8.0% faster
Get/segs50-shards512 71.94 65.40 9.1% faster
GetReplicated/segs50-shards8-reps2 174.2 161.8 7.1% faster
GetReplicated/segs50-shards32-reps2 181.1 164.1 9.4% faster
GetReplicated/segs50-shards128-reps2 201.0 176.3 12.3% faster
GetReplicated/segs50-shards512-reps2 242.9 203.8 16.1% faster
GetReplicated/segs50-shards8-reps4 380.1 341.0 10.3% faster
GetReplicated/segs50-shards32-reps4 391.8 353.1 9.9% faster
GetReplicated/segs50-shards128-reps4 442.8 373.6 15.6% faster
GetReplicated/segs50-shards512-reps4 533.6 446.2 16.4% faster
GetReplicated/segs50-shards8-reps8 1061.0 823.9 22.4% faster
GetReplicated/segs50-shards32-reps8 820.3 735.9 10.3% faster
GetReplicated/segs50-shards128-reps8 936.4 808.2 13.7% faster
GetReplicated/segs50-shards512-reps8 1109.0 1020.0 8.1% faster
Geometric Mean 11.5% faster

Memory allocations remain unchanged.

Motivation

Inspired by a similar optimization in groupcache (groupcache/groupcache-go#22). The key insight is that storing hash and owner together in a struct slice provides:

  • O(1) direct slice access instead of map lookup after binary search
  • Slightly reduced memory footprint (no map overhead)

@abhishek-singla-97 abhishek-singla-97 changed the title consistenthash: remove hashMap for optimized peer selection perf: remove hashMap for optimized peer selection Dec 27, 2025
Replace separate keyHashes []uint32 and hashMap map[uint32]string with
a single segments []segment struct slice. This eliminates the map lookup
overhead in the hot path (Get, findSegmentOwner).

Benchmark results show 7-22% performance improvement:

| Benchmark                     | Old (ns/op) | New (ns/op) | Speedup      |
|-------------------------------|-------------|-------------|--------------|
| Get/segs50-shards8            | 37.99       | 34.47       | 9.3% faster  |
| Get/segs50-shards32           | 40.66       | 37.41       | 8.0% faster  |
| Get/segs50-shards512          | 71.94       | 65.40       | 9.1% faster  |
| GetReplicated/shards8-reps2   | 174.2       | 161.8       | 7.1% faster  |
| GetReplicated/shards512-reps2 | 242.9       | 203.8       | 16.1% faster |
| GetReplicated/shards8-reps8   | 1061.0      | 823.9       | 22.4% faster |

Geometric mean: 11.5% faster
Memory allocations: unchanged
@abhishek-singla-97 abhishek-singla-97 force-pushed the optimize-consistenthash-eliminate-hashmap branch from 043796f to 1488f8d Compare December 27, 2025 06:04
@abhishek-singla-97
Copy link
Author

@dfinkel can you please take a look at this PR? I have mentioned the change in the description, which is a perf enhancement

@dfinkel
Copy link

dfinkel commented Jan 7, 2026

Thanks for sending this PR!

Sorry about the delay. (We were on vacation for the end of the year)
I just merged a change to reenable github actions on pull requests from forks, so if you rebase we can trigger the checks.

This does look like a nice improvement, and the code looks good at a first glance, I'll make a closer pass once the tests have passed.

By the way, it's helpful to link to or reference the PR and/or commit from upstream when porting patches or reimplementing them.
It looks like groupcache/groupcache-go#22 is the relevant PR from groupcache.

@abhishek-singla-97
Copy link
Author

sure @dfinkel
I have hyperlinked the PR in the description now.

Copy link

@dfinkel dfinkel left a comment

Choose a reason for hiding this comment

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

Looks good!
Just a couple minor nits.

Comment on lines 70 to 76
if a.hash < b.hash {
return -1
}
if a.hash > b.hash {
return 1
}
return 0
Copy link

Choose a reason for hiding this comment

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

this can be simplified by using cmp.Compare

slices.SortFunc(m.segments, func(a, b segment) int { return cmp.Compare(a.hash,b.hash)}

Copy link
Author

Choose a reason for hiding this comment

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

done

Comment on lines +44 to +47
hashToOwner := make(map[uint32]string, len(m.segments)+len(ownerKeys)*m.segsPerKey)
for _, seg := range m.segments {
hashToOwner[seg.hash] = seg.owner
}
Copy link

Choose a reason for hiding this comment

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

Let's add a comment saying that Add() is called several orders of magnitude less often than Get and GetReplicated, since hashrings should change slowly, so it's beneficial to trade a smaller Map against doing a bit more work when calling Add.

Copy link
Author

Choose a reason for hiding this comment

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

added the comment. Kept the wording quite similar to what you mentioned. Hope that's fine.

Copy link

Choose a reason for hiding this comment

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

Works perfectly 🙂

Copy link

@dfinkel dfinkel left a comment

Choose a reason for hiding this comment

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

LGTM
Thanks!

Comment on lines +44 to +47
hashToOwner := make(map[uint32]string, len(m.segments)+len(ownerKeys)*m.segsPerKey)
for _, seg := range m.segments {
hashToOwner[seg.hash] = seg.owner
}
Copy link

Choose a reason for hiding this comment

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

Works perfectly 🙂

@dfinkel dfinkel enabled auto-merge January 7, 2026 16:54
@dfinkel dfinkel merged commit 48ff582 into vimeo:master Jan 7, 2026
4 checks passed
@dfinkel
Copy link

dfinkel commented Jan 7, 2026

I've tagged v1.4.1 with this change:
https://github.com/vimeo/galaxycache/releases/tag/v1.4.1

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