Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize ZRANK to avoid path comparisons #1389

Merged
merged 8 commits into from
Dec 9, 2024

Conversation

ranshid
Copy link
Member

@ranshid ranshid commented Dec 4, 2024

ZRANK is a widly used command for workloads using sorted-sets. For example, in leaderboards It enables query the specific rank of a player.
The way ZRANK is currently implemented is:

  1. locate the element in the SortedSet hashtable.
  2. take the score of the element and use it in order to locate the element in the SkipList (when listpack encoding is not used)
  3. During the SkipLis scan for the elemnt we keep the path and use it in order to sum the span in each path node in order to calculate the elemnt rank

One problem with this approach is that it involves multiple compare operations in order to locate the element. Specifically string comparison can be expensive since it will require access multiple memory locations for the items the element string is compared against.
Perf analysis showed this can take up to 20% of the rank scan time

perf

We can improve the rank search by taking advantage of the fact that the element node in the skiplist is pointed by the hashtable value!
Our Skiplist implementation is using FatKeys, where each added node is assigned a randomly chosen height. Say we keep a height record for every skiplist element. In order to get an element rank we simply:

  1. locate the element in the SortedSet hashtable.
  2. we go directly to the node in the skiplist.
  3. we jump to the full height of the node and take the span value.
  4. we continue going foreward and always jump to the heighst point in each node we get to, making sure to sum all the spans.
  5. we take off the summed spans from the SkipList length and we now have the specific node rank :)

In order to test this method I created several benchmarks. All benchmarks used the same seeds and the lists contained 1M elements. Since a very important factor is the number of scores compared to the number of elements (since small ratio means more string compares during searches) each benchmark test used different number of scores (1, 10K, 100K, 1M)
some results:

TPS

Scores range non-optimized optimized gain
1 416042 605363 45.51%
10K 359776 459200 27.63%
100K 380387 459157 20.71%
1M 416059 450853 8.36%

Latency

Scores range non-optimized optimized gain
1 1.191000 0.831000 -30.23%
10K 1.383000 1.095000 -20.82%
100K 1.311000 1.087000 -17.09%
1M 1.191000 1.119000 -6.05%

Memory efficiency

adding another field to each skiplist node can cause degredation in memory efficiency for large sortedsets. We use the fact that level 0 recorded span of ALL nodes can either be 1 or zero (for the last node). So we use wrappers in order to get a node span and override the span for level 0 to hold the node height.

We can support a faster zrank by walking bottom-up instead of walk-downw

Signed-off-by: Ran Shidlansik <[email protected]>
Copy link

codecov bot commented Dec 4, 2024

Codecov Report

Attention: Patch coverage is 96.22642% with 2 lines in your changes missing coverage. Please review.

Project coverage is 70.84%. Comparing base (105509c) to head (30a976e).
Report is 11 commits behind head on unstable.

Files with missing lines Patch % Lines
src/t_zset.c 96.22% 2 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##           unstable    #1389      +/-   ##
============================================
+ Coverage     70.83%   70.84%   +0.01%     
============================================
  Files           118      118              
  Lines         63549    63595      +46     
============================================
+ Hits          45013    45055      +42     
- Misses        18536    18540       +4     
Files with missing lines Coverage Δ
src/server.h 100.00% <ø> (ø)
src/t_zset.c 95.63% <96.22%> (-0.03%) ⬇️

... and 15 files with indirect coverage changes

Signed-off-by: Ran Shidlansik <[email protected]>
Signed-off-by: Ran Shidlansik <[email protected]>
@ranshid ranshid marked this pull request as ready for review December 5, 2024 08:41
Signed-off-by: Ran Shidlansik <[email protected]>
Copy link
Contributor

@zuiderkwast zuiderkwast left a comment

Choose a reason for hiding this comment

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

Very smart idea.

Instead of looking up a skiplist node, we start at a node find the way out of the skip list.

What can we call this the inverse lookup? It deserves a cool name. 😎

@zuiderkwast zuiderkwast added release-notes This issue should get a line item in the release notes performance labels Dec 5, 2024
Signed-off-by: Ran Shidlansik <[email protected]>
Signed-off-by: Ran Shidlansik <[email protected]>
Signed-off-by: Ran Shidlansik <[email protected]>
Copy link
Contributor

@zuiderkwast zuiderkwast left a comment

Choose a reason for hiding this comment

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

I guess I'm good with merging this then.

Signed-off-by: Ran Shidlansik <[email protected]>
Copy link
Contributor

@zuiderkwast zuiderkwast left a comment

Choose a reason for hiding this comment

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

Perfect

@zuiderkwast zuiderkwast changed the title optimize zrank to avoid path comparisons Optimize ZRANK to avoid path comparisons Dec 9, 2024
@zuiderkwast zuiderkwast merged commit 5be4ce6 into valkey-io:unstable Dec 9, 2024
48 checks passed
vudiep411 pushed a commit to Autxmaton/valkey that referenced this pull request Dec 15, 2024
ZRANK is a widly used command for workloads using sorted-sets. For
example, in leaderboards It enables query the specific rank of a player.
The way ZRANK is currently implemented is:

1. locate the element in the SortedSet hashtable.
2. take the score of the element and use it in order to locate the
element in the SkipList (when listpack encoding is not used)
3. During the SkipLis scan for the elemnt we keep the path and use it in
order to sum the span in each path node in order to calculate the elemnt
rank

One problem with this approach is that it involves multiple compare
operations in order to locate the element. Specifically string
comparison can be expensive since it will require access multiple memory
locations for the items the element string is compared against.
Perf analysis showed this can take up to 20% of the rank scan time. (TBD
- provide the perf results for example)

We can improve the rank search by taking advantage of the fact that the
element node in the skiplist is pointed by the hashtable value!
Our Skiplist implementation is using FatKeys, where each added node is
assigned a randomly chosen height. Say we keep a height record for every
skiplist element. In order to get an element rank we simply:

1. locate the element in the SortedSet hashtable.
2. we go directly to the node in the skiplist.
3. we jump to the full height of the node and take the span value.
4. we continue going foreward and always jump to the heighst point in
each node we get to, making sure to sum all the spans.
5. we take off the summed spans from the SkipList length and we now have
the specific node rank. :)

In order to test this method I created several benchmarks. All
benchmarks used the same seeds and the lists contained 1M elements.
Since a very important factor is the number of scores compared to the
number of elements (since small ratio means more string compares during
searches) each benchmark test used different number of scores (1, 10K,
100K, 1M)
some results:

**TPS**

Scores range | non-optimized | optimized | gain
-- | -- | -- | --
1 | 416042 | 605363 | 45.51%
10K | 359776 | 459200 | 27.63%
100K | 380387 | 459157 | 20.71%
1M | 416059 | 450853 | 8.36%

**Latency**

Scores range | non-optimized | optimized | gain
-- | -- | -- | --
1 | 1.191000 | 0.831000 | -30.23%
10K | 1.383000 | 1.095000 | -20.82%
100K | 1.311000 | 1.087000 | -17.09%
1M | 1.191000 | 1.119000 | -6.05%

###  Memory efficiency

adding another field to each skiplist node can cause degredation in
memory efficiency for large sortedsets. We use the fact that level 0
recorded span of ALL nodes can either be 1 or zero (for the last node).
So we use wrappers in order to get a node span and override the span for
level 0 to hold the node height.

---------

Signed-off-by: Ran Shidlansik <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
performance release-notes This issue should get a line item in the release notes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants