Skip to content

Commit

Permalink
Minor optimisations to zset
Browse files Browse the repository at this point in the history
Instead of storing a separate SortedDict sorted lexically, it just
stores a regular dict to look up scores. The *BYLEX commands now take
advantage of the assumption that all scores are equal by searching in
the _byscore list, using the score of the first key.

This makes a very small improvement in performance, and also reduces
memory usage.
  • Loading branch information
bmerry committed Jan 14, 2019
1 parent 166b2f6 commit e22a205
Show file tree
Hide file tree
Showing 2 changed files with 30 additions and 18 deletions.
8 changes: 4 additions & 4 deletions fakeredis/_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1889,19 +1889,19 @@ def _apply_withscores(items, withscores):
@command((Key(ZSet), bytes, bytes), (bytes,))
def zadd(self, key, *args):
# TODO: handle NX, XX, CH, INCR
zset = key.value
if len(args) % 2 != 0:
raise redis.ResponseError(SYNTAX_ERROR_MSG)
items = []
# Parse all scores first, before updating
for i in range(0, len(args), 2):
score = Float.decode(args[i])
items.append((score, args[i + 1]))
old_len = len(key.value)
old_len = len(zset)
for item in items:
if item[1] not in key.value or key.value[item[1]] != item[0]:
key.value[item[1]] = item[0]
if zset.add(item[1], item[0]):
key.updated()
return len(key.value) - old_len
return len(zset) - old_len

@command((Key(ZSet),))
def zcard(self, key):
Expand Down
40 changes: 26 additions & 14 deletions fakeredis/_zset.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,25 @@

class ZSet(object):
def __init__(self):
self._bylex = sortedcontainers.SortedDict()
self._byscore = sortedcontainers.SortedSet()
self._bylex = {} # Maps value to score
self._byscore = sortedcontainers.SortedList()

def __contains__(self, value):
return value in self._bylex

def __setitem__(self, value, score):
try:
old_score = self._bylex[value]
except KeyError:
pass
else:
self._byscore.discard((old_score, value))
def add(self, value, score):
"""Update the item and return whether it modified the zset"""
old_score = self._bylex.get(value, None)
if old_score is not None:
if score == old_score:
return False
self._byscore.remove((old_score, value))
self._bylex[value] = score
self._byscore.add((score, value))
return True

def __setitem__(self, value, score):
self.add(value, score)

def __getitem__(self, key):
return self._bylex[key]
Expand Down Expand Up @@ -49,21 +53,29 @@ def zcount(self, min_, max_):
return max(0, pos2 - pos1)

def zlexcount(self, min_value, min_exclusive, max_value, max_exclusive):
if not self._byscore:
return 0
score = self._byscore[0][0]
if min_exclusive:
pos1 = self._bylex.bisect_right(min_value)
pos1 = self._byscore.bisect_right((score, min_value))
else:
pos1 = self._bylex.bisect_left(min_value)
pos1 = self._byscore.bisect_left((score, min_value))
if max_exclusive:
pos2 = self._bylex.bisect_left(max_value)
pos2 = self._byscore.bisect_left((score, max_value))
else:
pos2 = self._bylex.bisect_right(max_value)
pos2 = self._byscore.bisect_right((score, max_value))
return max(0, pos2 - pos1)

def islice_score(self, start, stop, reverse=False):
return self._byscore.islice(start, stop, reverse)

def irange_lex(self, start, stop, inclusive=(True, True), reverse=False):
return self._bylex.irange(start, stop, inclusive=inclusive, reverse=reverse)
if not self._byscore:
return iter([])
score = self._byscore[0][0]
it = self._byscore.irange((score, start), (score, stop),
inclusive=inclusive, reverse=reverse)
return (item[1] for item in it)

def irange_score(self, start, stop, reverse=False):
return self._byscore.irange(start, stop, reverse=reverse)
Expand Down

0 comments on commit e22a205

Please sign in to comment.