summaryrefslogtreecommitdiff
path: root/src/quicklist.c
diff options
context:
space:
mode:
authorsundb <sundbcn@gmail.com>2022-11-17 02:29:46 +0800
committerGitHub <noreply@github.com>2022-11-16 20:29:46 +0200
commit2168ccc661791ced6271c5e4ab0f5eb60b1559e2 (patch)
treeb9db3781b460230d170b8454c7c8a6e719b03a57 /src/quicklist.c
parentd136bf28307ed2add5a0b709586433f4cffd70a7 (diff)
downloadredis-2168ccc661791ced6271c5e4ab0f5eb60b1559e2.tar.gz
Add listpack encoding for list (#11303)
Improve memory efficiency of list keys ## Description of the feature The new listpack encoding uses the old `list-max-listpack-size` config to perform the conversion, which we can think it of as a node inside a quicklist, but without 80 bytes overhead (internal fragmentation included) of quicklist and quicklistNode structs. For example, a list key with 5 items of 10 chars each, now takes 128 bytes instead of 208 it used to take. ## Conversion rules * Convert listpack to quicklist When the listpack length or size reaches the `list-max-listpack-size` limit, it will be converted to a quicklist. * Convert quicklist to listpack When a quicklist has only one node, and its length or size is reduced to half of the `list-max-listpack-size` limit, it will be converted to a listpack. This is done to avoid frequent conversions when we add or remove at the bounding size or length. ## Interface changes 1. add list entry param to listTypeSetIteratorDirection When list encoding is listpack, `listTypeIterator->lpi` points to the next entry of current entry, so when changing the direction, we need to use the current node (listTypeEntry->p) to update `listTypeIterator->lpi` to the next node in the reverse direction. ## Benchmark ### Listpack VS Quicklist with one node * LPUSH - roughly 0.3% improvement * LRANGE - roughly 13% improvement ### Both are quicklist * LRANGE - roughly 3% improvement * LRANGE without pipeline - roughly 3% improvement From the benchmark, as we can see from the results 1. When list is quicklist encoding, LRANGE improves performance by <5%. 2. When list is listpack encoding, LRANGE improves performance by ~13%, the main enhancement is brought by `addListListpackRangeReply()`. ## Memory usage 1M lists(key:0~key:1000000) with 5 items of 10 chars ("hellohello") each. shows memory usage down by 35.49%, from 214MB to 138MB. ## Note 1. Add conversion callback to support doing some work before conversion Since the quicklist iterator decompresses the current node when it is released, we can no longer decompress the quicklist after we convert the list.
Diffstat (limited to 'src/quicklist.c')
-rw-r--r--src/quicklist.c71
1 files changed, 39 insertions, 32 deletions
diff --git a/src/quicklist.c b/src/quicklist.c
index 954331365..301a2166e 100644
--- a/src/quicklist.c
+++ b/src/quicklist.c
@@ -30,6 +30,7 @@
#include <stdio.h>
#include <string.h> /* for memcpy */
+#include <limits.h>
#include "quicklist.h"
#include "zmalloc.h"
#include "config.h"
@@ -447,25 +448,45 @@ REDIS_STATIC void _quicklistInsertNodeAfter(quicklist *quicklist,
__quicklistInsertNode(quicklist, old_node, new_node, 1);
}
-REDIS_STATIC int
-_quicklistNodeSizeMeetsOptimizationRequirement(const size_t sz,
- const int fill) {
- if (fill >= 0)
- return 0;
+#define sizeMeetsSafetyLimit(sz) ((sz) <= SIZE_SAFETY_LIMIT)
- size_t offset = (-fill) - 1;
- if (offset < (sizeof(optimization_level) / sizeof(*optimization_level))) {
- if (sz <= optimization_level[offset]) {
- return 1;
- } else {
- return 0;
- }
+/* Calculate the size limit or length limit of the quicklist node
+ * based on 'fill', and is also used to limit list listpack. */
+void quicklistNodeLimit(int fill, size_t *size, unsigned int *count) {
+ *size = SIZE_MAX;
+ *count = UINT_MAX;
+
+ if (fill >= 0) {
+ /* Ensure that one node have at least one entry */
+ *count = (fill == 0) ? 1 : fill;
} else {
- return 0;
+ size_t offset = (-fill) - 1;
+ size_t max_level = sizeof(optimization_level) / sizeof(*optimization_level);
+ if (offset >= max_level) offset = max_level - 1;
+ *size = optimization_level[offset];
}
}
-#define sizeMeetsSafetyLimit(sz) ((sz) <= SIZE_SAFETY_LIMIT)
+/* Check if the limit of the quicklist node has been reached to determine if
+ * insertions, merges or other operations that would increase the size of
+ * the node can be performed.
+ * Return 1 if exceeds the limit, otherwise 0. */
+int quicklistNodeExceedsLimit(int fill, size_t new_sz, unsigned int new_count) {
+ size_t sz_limit;
+ unsigned int count_limit;
+ quicklistNodeLimit(fill, &sz_limit, &count_limit);
+
+ if (likely(sz_limit != SIZE_MAX)) {
+ return new_sz > sz_limit;
+ } else if (count_limit != UINT_MAX) {
+ /* when we reach here we know that the limit is a size limit (which is
+ * safe, see comments next to optimization_level and SIZE_SAFETY_LIMIT) */
+ if (!sizeMeetsSafetyLimit(new_sz)) return 1;
+ return new_count > count_limit;
+ }
+
+ redis_unreachable();
+}
REDIS_STATIC int _quicklistNodeAllowInsert(const quicklistNode *node,
const int fill, const size_t sz) {
@@ -481,16 +502,9 @@ REDIS_STATIC int _quicklistNodeAllowInsert(const quicklistNode *node,
* Note: No need to check for overflow below since both `node->sz` and
* `sz` are to be less than 1GB after the plain/large element check above. */
size_t new_sz = node->sz + sz + SIZE_ESTIMATE_OVERHEAD;
- if (likely(_quicklistNodeSizeMeetsOptimizationRequirement(new_sz, fill)))
- return 1;
- /* when we return 1 above we know that the limit is a size limit (which is
- * safe, see comments next to optimization_level and SIZE_SAFETY_LIMIT) */
- else if (!sizeMeetsSafetyLimit(new_sz))
- return 0;
- else if ((int)node->count < fill)
- return 1;
- else
+ if (unlikely(quicklistNodeExceedsLimit(fill, new_sz, node->count + 1)))
return 0;
+ return 1;
}
REDIS_STATIC int _quicklistNodeAllowMerge(const quicklistNode *a,
@@ -505,16 +519,9 @@ REDIS_STATIC int _quicklistNodeAllowMerge(const quicklistNode *a,
/* approximate merged listpack size (- 7 to remove one listpack
* header/trailer, see LP_HDR_SIZE and LP_EOF) */
unsigned int merge_sz = a->sz + b->sz - 7;
- if (likely(_quicklistNodeSizeMeetsOptimizationRequirement(merge_sz, fill)))
- return 1;
- /* when we return 1 above we know that the limit is a size limit (which is
- * safe, see comments next to optimization_level and SIZE_SAFETY_LIMIT) */
- else if (!sizeMeetsSafetyLimit(merge_sz))
- return 0;
- else if ((int)(a->count + b->count) <= fill)
- return 1;
- else
+ if (unlikely(quicklistNodeExceedsLimit(fill, merge_sz, a->count + b->count)))
return 0;
+ return 1;
}
#define quicklistNodeUpdateSz(node) \