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

Accelerate hash table iterator with value prefetching #1568

Open
wants to merge 1 commit into
base: unstable
Choose a base branch
from

Conversation

NadavGigi
Copy link
Contributor

This PR builds upon the previous entry prefetching optimization to further enhance performance by implementing value prefetching for hashtable iterators.

Implementation

Modified hashtableNext to accept a new flags parameter, allowing control over iterator behavior.
Implemented conditional value prefetching within hashtableNext based on the new HASHTABLE_ITER_PREFETCH_VALUES flag.
When the flag is set, hashtableNext now calls prefetchBucketValues at the start of each new bucket, preemptively loading the values of filled entries into the CPU cache.
The actual prefetching of values is performed using type-specific callback functions implemented in server.c:

  • For robj the hashtableObjectPrefetchValue callback is used to prefetch the value if not embeded.
  • For hashTypeEntry the hashHashtableTypePrefetchValue callback is used to prefetch the sds value.

Updated relevant parts of the codebase (e.g., in aof.c, debug.c, rdb.c) to utilize this new prefetching capability where appropriate.

Currently supports value prefetching for hashtables storing robj and hashTypeEntry types, laying the groundwork for future extensions to additional data types.

Performance

Setup:

  • 64cores Graviton 3 Amazon EC2 instance.
  • 50 mil keys with different value sizes.
  • Running valkey server over RAM file system.
  • crc checksum and comperssion off.

Action

  • save command.

Results

The results regarding the duration of “save” command was taken from “info all” command.

+--------------------+------------------+------------------+ 
| Prefetching        | Value size (byte)| Time (seconds)   | 
+--------------------+------------------+------------------+ 
| No                 | 100              | 20.112279        | 
| Yes                | 100              | 12.758519        | 
| No                 | 40               | 16.945366        | 
| Yes                | 40               | 10.902022        |
| No                 | 20               | 9.817000         | 
| Yes                | 20               | 9.626821         |
| No                 | 10               | 9.71510          | 
| Yes                | 10               | 9.510565         |
+--------------------+------------------+------------------+

The results largely align with our expectations, showing significant improvements for larger values (100 bytes and 40 bytes) that are stored outside the robj. For smaller values (20 bytes and 10 bytes) that are embedded within the robj, we see almost no improvement, which is as expected.

However, the small improvement observed even for these embedded values is somewhat surprising. Given that we are not actively prefetching these embedded values, this minor performance gain was not anticipated.

perf record on save command without value prefetching:

                --99.98%--rdbSaveDb
                          |          
                          |--91.38%--rdbSaveKeyValuePair
                          |          |          
                          |          |--42.72%--rdbSaveRawString
                          |          |          |          
                          |          |          |--26.69%--rdbWriteRaw
                          |          |          |          |          
                          |          |          |           --25.75%--rioFileWrite.lto_priv.0
                          |          |          |          
                          |          |           --15.41%--rdbSaveLen
                          |          |                     |          
                          |          |                     |--7.58%--rdbWriteRaw
                          |          |                     |          |          
                          |          |                     |           --7.08%--rioFileWrite.lto_priv.0
                          |          |                     |                     |          
                          |          |                     |                      --6.54%--_IO_fwrite
                          |          |                     |                                         
                          |          |                     |          
                          |          |                      --7.42%--rdbWriteRaw.constprop.1
                          |          |                                |          
                          |          |                                 --7.18%--rioFileWrite.lto_priv.0
                          |          |                                           |          
                          |          |                                            --6.73%--_IO_fwrite
                          |          |                                                            
                          |          |          
                          |          |--40.44%--rdbSaveStringObject
                          |          |          
                          |           --7.62%--rdbSaveObjectType
                          |                     |          
                          |                      --7.39%--rdbWriteRaw.constprop.1
                          |                                |          
                          |                                 --7.04%--rioFileWrite.lto_priv.0
                          |                                           |          
                          |                                            --6.59%--_IO_fwrite
                          |                                                               
                          |          
                           --7.33%--hashtableNext.constprop.1
                                     |          
                                      --6.28%--prefetchNextBucketEntries.lto_priv.0

perf record on save command with value prefetching:

               rdbSaveRio
               |          
                --99.93%--rdbSaveDb
                          |          
                          |--79.81%--rdbSaveKeyValuePair
                          |          |          
                          |          |--66.79%--rdbSaveRawString
                          |          |          |          
                          |          |          |--42.31%--rdbWriteRaw
                          |          |          |          |          
                          |          |          |           --40.74%--rioFileWrite.lto_priv.0
                          |          |          |          
                          |          |           --23.37%--rdbSaveLen
                          |          |                     |          
                          |          |                     |--11.78%--rdbWriteRaw
                          |          |                     |          |          
                          |          |                     |           --11.03%--rioFileWrite.lto_priv.0
                          |          |                     |                     |          
                          |          |                     |                      --10.30%--_IO_fwrite
                          |          |                     |                                |          
                          |          |                     |          
                          |          |                      --10.98%--rdbWriteRaw.constprop.1
                          |          |                                |          
                          |          |                                 --10.44%--rioFileWrite.lto_priv.0
                          |          |                                           |          
                          |          |                                            --9.74%--_IO_fwrite
                          |          |                                                      |          
                          |          |          
                          |          |--11.33%--rdbSaveObjectType
                          |          |          |          
                          |          |           --10.96%--rdbWriteRaw.constprop.1
                          |          |                     |          
                          |          |                      --10.51%--rioFileWrite.lto_priv.0
                          |          |                                |          
                          |          |                                 --9.75%--_IO_fwrite
                          |          |                                           |          
                          |          |          
                          |           --0.77%--rdbSaveStringObject
                          |          
                           --18.39%--hashtableNext
                                     |          
                                     |--10.04%--hashtableObjectPrefetchValue
                                     |
                                      --6.06%--prefetchNextBucketEntries        

Conclusions:

The prefetching strategy appears to be working as intended, shifting the performance bottleneck from data access to I/O operations.
The significant reduction in rdbSaveStringObject time suggests that string objects(which are the values) are being accessed more efficiently.

@NadavGigi NadavGigi changed the title Improving iterator using prefetch values Accelerate hash table iterator with value prefetching Jan 15, 2025
Copy link

codecov bot commented Jan 15, 2025

Codecov Report

Attention: Patch coverage is 93.33333% with 4 lines in your changes missing coverage. Please review.

Project coverage is 70.75%. Comparing base (2a1a65b) to head (35f6b6e).
Report is 1 commits behind head on unstable.

Files with missing lines Patch % Lines
src/module.c 0.00% 2 Missing ⚠️
src/acl.c 75.00% 1 Missing ⚠️
src/server.c 94.44% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##           unstable    #1568      +/-   ##
============================================
- Coverage     70.78%   70.75%   -0.04%     
============================================
  Files           120      120              
  Lines         65046    65079      +33     
============================================
+ Hits          46045    46048       +3     
- Misses        19001    19031      +30     
Files with missing lines Coverage Δ
src/aof.c 80.23% <100.00%> (ø)
src/db.c 89.56% <100.00%> (ø)
src/debug.c 52.40% <100.00%> (ø)
src/hashtable.c 77.59% <100.00%> (+0.24%) ⬆️
src/kvstore.c 95.18% <100.00%> (ø)
src/latency.c 80.92% <100.00%> (ø)
src/object.c 82.04% <100.00%> (ø)
src/rdb.c 76.58% <100.00%> (-0.33%) ⬇️
src/sort.c 94.82% <100.00%> (ø)
src/t_hash.c 96.13% <100.00%> (ø)
... and 5 more

... and 10 files with indirect coverage changes

@@ -657,7 +657,7 @@ void ACLChangeSelectorPerm(aclSelector *selector, struct serverCommand *cmd, int
hashtableIterator iter;
hashtableInitSafeIterator(&iter, cmd->subcommands_ht);
void *next;
while (hashtableNext(&iter, &next)) {
while (hashtableNext(&iter, &next, 0)) {
Copy link
Member

Choose a reason for hiding this comment

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

Why aren't we prefetching in these cases?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In this pr i have added support only for hashtables that storing robj and hashTypeEntry.
I will add support for more complex data structures in a different pr.

Copy link
Contributor

Choose a reason for hiding this comment

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

This makes the hashtableNext call less simple and the change affects all places even where it is not used. I have another suggestions: storing the iterator flags in the iterator:

        hashtableIterator iter;
        hashtableInitSafeIterator(&iter, cmd->subcommands_ht);
        hashtableIteratorSetFlags(HASHTABLE_ITER_PREFETCH_VALUES);
        void *next;
        while (hashtableNext(&iter, &next)) {

Alternatively, we could set the prefetch callback directly in the iterator instead of storing it in the hashtableType.

        hashtableIteratorSetPrefetchCallback(&iter, hashTypeEntryPrefetchValue);

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.

Interesting idea. I just skimmed though, not a full review.

To summarize, this is a second level of pre-fetching. Theoretically, we could do even more levels, right? It could even be generalized to any number of levels. We could use an array of prefetch callbacks.... (not saying we should)

We should probably also consider the bigger picture where this kind of callbacks can be used not only for the iterator, but also for the hashtableIncrementalFind that's already used with IO threads, and with other operations like Scan.

A hypothetical use case: Imagine we have a sorted set where we want to find the 10 members sorted by score, ZRANGE. We could find the starting element and also prefetch in up to 10 steps the next elements.

Have you been thinking about more prefetching ideas? Can you share your ideas? It's useful to understand the bigger picture better before we add many small optimizations for specific scenarios.

@@ -657,7 +657,7 @@ void ACLChangeSelectorPerm(aclSelector *selector, struct serverCommand *cmd, int
hashtableIterator iter;
hashtableInitSafeIterator(&iter, cmd->subcommands_ht);
void *next;
while (hashtableNext(&iter, &next)) {
while (hashtableNext(&iter, &next, 0)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This makes the hashtableNext call less simple and the change affects all places even where it is not used. I have another suggestions: storing the iterator flags in the iterator:

        hashtableIterator iter;
        hashtableInitSafeIterator(&iter, cmd->subcommands_ht);
        hashtableIteratorSetFlags(HASHTABLE_ITER_PREFETCH_VALUES);
        void *next;
        while (hashtableNext(&iter, &next)) {

Alternatively, we could set the prefetch callback directly in the iterator instead of storing it in the hashtableType.

        hashtableIteratorSetPrefetchCallback(&iter, hashTypeEntryPrefetchValue);

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