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 range deletion in zset encoded with listpack #13132

Open
wants to merge 8 commits into
base: unstable
Choose a base branch
from

Conversation

lyq2333
Copy link
Contributor

@lyq2333 lyq2333 commented Mar 13, 2024

In past, if we delete a range of elements in zset encoded with listpack, we call lpDeleteRangeWithEntry repeatedly. But lpDeleteRangeWithEntry calls lpShrinkToFit in which realloc is called. This means we call many unnecessary realloc.

In this PR, we optimize zremrangeby[score|lex] and zpop[min|max] by deleting all elements once to avoid redundant realloc. We add a new function named lpDeleteRangeWithEntryPtr to delete the range of elements from the start address to the tail address and avoid duplicate traversal search.

The result shows that the performance of zremrangeby[score|lex] can improve about 70% and the performance of zpop[min|max] can improve about 8%.

zremrangebyscore

I set 500,000 zset keys and each has 100 elements. Then I use zremrangebyscore to delete all. The result is as follow. The performance of range deletion increases about 70%.
Unstable

Summary:
  throughput summary: 39326.73 requests per second
  latency summary (msec):
          avg       min       p50       p95       p99       max
        1.180     0.280     1.167     1.591     2.375     4.447

This PR

Summary:
  throughput summary: 68101.34 requests per second
  latency summary (msec):
          avg       min       p50       p95       p99       max
        0.658     0.160     0.639     0.871     0.927     3.063

zpopmin

zpop[min|max] has the same problem. I set 500,000 zset keys and each has 100 elements. Then I use zpopmin to delete all. The result is as follow. The performance of range deletion increases about 8%.
Unstable

Summary:
  throughput summary: 21452.78 requests per second
  latency summary (msec):
          avg       min       p50       p95       p99       max
        1.894     0.696     1.847     2.743     2.935     4.855

This PR

Summary:
  throughput summary: 23104.83 requests per second
  latency summary (msec):
          avg       min       p50       p95       p99       max
        1.105     0.528     1.095     1.167     1.511     4.471

@lyq2333
Copy link
Contributor Author

lyq2333 commented Mar 13, 2024

@sundb I'd appreciate it if you could review this pr.

Copy link
Collaborator

@sundb sundb left a comment

Choose a reason for hiding this comment

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

LGTM

src/t_zset.c Outdated Show resolved Hide resolved
src/t_zset.c Outdated Show resolved Hide resolved
src/t_zset.c Outdated Show resolved Hide resolved
src/t_zset.c Outdated Show resolved Hide resolved
sundb
sundb previously approved these changes Mar 13, 2024
@lyq2333
Copy link
Contributor Author

lyq2333 commented Mar 13, 2024

It seems that our sanitizer CI has some problems.
image

@sundb
Copy link
Collaborator

sundb commented Mar 13, 2024

@lyq2333 it's nothing to do with this PR, i found it yesterday but don't know why.

src/t_zset.c Outdated
} else {
/* No longer in range. */
break;
}
}

if (num)
zl = lpDeleteRangeWithEntry(zl, &first_ele, 2 * num);
Copy link
Collaborator

Choose a reason for hiding this comment

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

After the above loop, we can get the range's tail, which is eptr (if NULL it means it's the last one in listpack). I think we could implement another function lpDeleteRangeWithEntryPtr(unsigned char *lp, unsigned char **first, unsigned char **tail) to avoid the traversal search for tail within lpDeleteRangeWithEntry.

Copy link
Collaborator

Choose a reason for hiding this comment

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

In this case we also need to pass the entry number between first and tail, otherwise we can't update the entry numbers, but that also make this method a little odd.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I find we have the same problem in zpopmin/max with count. In zpopmin/max, we also get the start position, end position and the number elements to be removed. Shall I add a new function like lpDeleteRangeWithEntryPtr to solve both of them?

Copy link
Collaborator

Choose a reason for hiding this comment

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

@lyq2333 we can go ahead with this way, we can compromise on it if it really brings a boost.

Copy link
Contributor Author

@lyq2333 lyq2333 Mar 18, 2024

Choose a reason for hiding this comment

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

@sundb @soloestoy I add the new function lpDeleteRangeWithEntryPtr and make a test. The result is as follow. It improve about 6% based on the origin branch(not unstable).

Before

Summary:
  throughput summary: 64333.51 requests per second
  latency summary (msec):
          avg       min       p50       p95       p99       max
        0.700     0.152     0.679     0.943     1.023     2.279

Now

Summary:
  throughput summary: 68101.34 requests per second
  latency summary (msec):
          avg       min       p50       p95       p99       max
        0.658     0.160     0.639     0.871     0.927     3.063

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I also make a test about zpopmin. I put the result in the top comment. The performance improvement is about 8% because we costs many time on reply.

src/t_zset.c Outdated
@@ -4035,11 +4062,11 @@ void genericZpopCommand(client *c, robj **keyv, int keyc, int where, int emitkey
serverAssertWithInfo(c,zobj,zln != NULL);
ele = sdsdup(zln->ele);
score = zln->score;
serverAssertWithInfo(c,zobj,zsetDel(zobj,ele));
Copy link
Collaborator

Choose a reason for hiding this comment

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

we can call zslDeleteRangeByRank after the whole fake pop loop.

serverAssertWithInfo(c,zobj,sptr != NULL);
score = zzlGetScore(sptr);
/* In previous versions, we call notifyKeyspaceEvent after the first entry is deleted.
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we can move the notifyKeyspaceEvent to after all the pop completed. This way, we won't need to check the result_count, and the logic will be much clearer. Commands like zremrange* operate in the same manner since the execution of the command is atomic.

module is a bit special, but I believe that moving the notification from after the first element is popped to after all elements have been popped will not break compatibility with modules.

I'd also like to ping @oranagra to confirm this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I also think notification immediately after removing the first element is weird. But I don't know the reason.

Copy link
Member

Choose a reason for hiding this comment

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

indeed the logic of firing the notification only on the first removal seems odd (rather than firing on every removal).
when we realize that when this code was created in 56bbab2 there were no modules, then firing it on the first removal was equivalent to firing it at the end (if there was at least one removal).
but with modules it makes less sense.
i'd vote to move this to the end and document the behavior change for modules.
@MeirShpilraien WDYT?

src/t_zset.c Outdated
@@ -4035,11 +4062,11 @@ void genericZpopCommand(client *c, robj **keyv, int keyc, int where, int emitkey
serverAssertWithInfo(c,zobj,zln != NULL);
ele = sdsdup(zln->ele);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Another minor optimization: for the skiplist, we don't need to copy the ele. Or perhaps we should change it so that listpack and skiplist handle their own loops respectively.

unsigned long poff = *first-lp;

/* Move tail to the front of the listpack */
memmove(*first, tail, eofptr - tail + 1);
Copy link
Collaborator

Choose a reason for hiding this comment

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

i'm worried about about this, if the bytes is corrupted, does this mean it's possible to use eofptr to read the memory data after this listpack?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I guess that one reason of the performance improvement is the reduction calls of lpAssertValidEntry.

Copy link
Contributor Author

@lyq2333 lyq2333 Mar 18, 2024

Choose a reason for hiding this comment

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

In lpDeleteRangeWithEntry, if the bytes is corrupted, we also have the same problem. If we want to avoid this problem, I think maybe we have to travel the whole listpack to verify the validity of the bytes.

We get first and tail by lpNext or lpPrev, in which we have called lpAssertValidEntry. I think maybe we don't need to repeat it in lpDeleteRangeWithEntryPtr.

Copy link
Collaborator

Choose a reason for hiding this comment

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

yeah, you're probably right, please merge the unstable and i'll run a fully CI to check it.

@sundb
Copy link
Collaborator

sundb commented Mar 18, 2024

@lyq2333 please have a look at these crash in the following fully CI:
https://github.com/sundb/redis/actions/runs/8325132244/job/22781563626
https://github.com/sundb/redis/actions/runs/8325132244/job/22778109249

please ignore this, I saw it in other places.

@lyq2333
Copy link
Contributor Author

lyq2333 commented Mar 19, 2024

@lyq2333 please have a look at these crash in the following fully CI: https://github.com/sundb/redis/actions/runs/8325132244/job/22781563626 https://github.com/sundb/redis/actions/runs/8325132244/job/22778109249 please ignore this, I saw it in other places.

@sundb I think maybe this is because #13154. PTAL.

@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: In Review
Development

Successfully merging this pull request may close these issues.

None yet

5 participants