Skip to content

Commit 5094118

Browse files
mykaulLorak-mmk
authored andcommitted
perf: use stdlib bisect and attrgetter in tablets.py
- Use bisect.bisect_left from stdlib unconditionally (C implementation); drop the bundled pure-Python fallback since we only support Python 3.10+ - Replace per-call lambda closures with module-level operator.attrgetter for first_token/last_token extraction - Add unit tests for get_tablet_for_key Benchmark results (get_tablet_for_key hit): 10 tablets: 517 ns -> 365 ns (1.42x) 100 tablets: 616 ns -> 351 ns (1.75x) 1000 tablets: 1008 ns -> 529 ns (1.91x) 10000 tablets: 1339 ns -> 610 ns (2.20x)
1 parent db317eb commit 5094118

2 files changed

Lines changed: 47 additions & 39 deletions

File tree

cassandra/tablets.py

Lines changed: 9 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1+
from bisect import bisect_left
2+
from operator import attrgetter
13
from threading import Lock
24
from typing import Optional
35
from uuid import UUID
46

7+
# C-accelerated attrgetter avoids per-call lambda allocation overhead
8+
_get_first_token = attrgetter("first_token")
9+
_get_last_token = attrgetter("last_token")
10+
511

612
class Tablet(object):
713
"""
@@ -57,7 +63,7 @@ def get_tablet_for_key(self, keyspace, table, t):
5763
if not tablet:
5864
return None
5965

60-
id = bisect_left(tablet, t.value, key=lambda tablet: tablet.last_token)
66+
id = bisect_left(tablet, t.value, key=_get_last_token)
6167
if id < len(tablet) and t.value > tablet[id].first_token:
6268
return tablet[id]
6369
return None
@@ -94,12 +100,12 @@ def add_tablet(self, keyspace, table, tablet):
94100
tablets_for_table = self._tablets.setdefault((keyspace, table), [])
95101

96102
# find first overlapping range
97-
start = bisect_left(tablets_for_table, tablet.first_token, key=lambda t: t.first_token)
103+
start = bisect_left(tablets_for_table, tablet.first_token, key=_get_first_token)
98104
if start > 0 and tablets_for_table[start - 1].last_token > tablet.first_token:
99105
start = start - 1
100106

101107
# find last overlapping range
102-
end = bisect_left(tablets_for_table, tablet.last_token, key=lambda t: t.last_token)
108+
end = bisect_left(tablets_for_table, tablet.last_token, key=_get_last_token)
103109
if end < len(tablets_for_table) and tablets_for_table[end].first_token >= tablet.last_token:
104110
end = end - 1
105111

@@ -108,39 +114,3 @@ def add_tablet(self, keyspace, table, tablet):
108114

109115
tablets_for_table.insert(start, tablet)
110116

111-
112-
# bisect.bisect_left implementation from Python 3.11, needed untill support for
113-
# Python < 3.10 is dropped, it is needed to use `key` to extract last_token from
114-
# Tablet list - better solution performance-wise than materialize list of last_tokens
115-
def bisect_left(a, x, lo=0, hi=None, *, key=None):
116-
"""Return the index where to insert item x in list a, assuming a is sorted.
117-
118-
The return value i is such that all e in a[:i] have e < x, and all e in
119-
a[i:] have e >= x. So if x already appears in the list, a.insert(i, x) will
120-
insert just before the leftmost x already there.
121-
122-
Optional args lo (default 0) and hi (default len(a)) bound the
123-
slice of a to be searched.
124-
"""
125-
126-
if lo < 0:
127-
raise ValueError('lo must be non-negative')
128-
if hi is None:
129-
hi = len(a)
130-
# Note, the comparison uses "<" to match the
131-
# __lt__() logic in list.sort() and in heapq.
132-
if key is None:
133-
while lo < hi:
134-
mid = (lo + hi) // 2
135-
if a[mid] < x:
136-
lo = mid + 1
137-
else:
138-
hi = mid
139-
return
140-
while lo < hi:
141-
mid = (lo + hi) // 2
142-
if key(a[mid]) < x:
143-
lo = mid + 1
144-
else:
145-
hi = mid
146-
return lo

tests/unit/test_tablets.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,41 @@ def test_add_tablet_intersecting_with_last(self):
8686

8787
self.compare_ranges(tablets_list, [(-8611686018427387905, -7917529027641081857),
8888
(-5011686018427387905, -2987529027641081857)])
89+
90+
91+
class GetTabletForKeyTest(unittest.TestCase):
92+
"""Tests for Tablets.get_tablet_for_key."""
93+
94+
def test_found(self):
95+
t1 = Tablet(0, 100, [("host1", 0)])
96+
t2 = Tablet(100, 200, [("host2", 0)])
97+
t3 = Tablet(200, 300, [("host3", 0)])
98+
tablets = Tablets({("ks", "tb"): [t1, t2, t3]})
99+
100+
class Token:
101+
def __init__(self, v):
102+
self.value = v
103+
104+
result = tablets.get_tablet_for_key("ks", "tb", Token(150))
105+
self.assertIs(result, t2)
106+
107+
def test_not_found_empty(self):
108+
tablets = Tablets({})
109+
110+
class Token:
111+
def __init__(self, v):
112+
self.value = v
113+
114+
self.assertIsNone(tablets.get_tablet_for_key("ks", "tb", Token(50)))
115+
116+
def test_not_found_outside_range(self):
117+
t1 = Tablet(100, 200, [("host1", 0)])
118+
tablets = Tablets({("ks", "tb"): [t1]})
119+
120+
class Token:
121+
def __init__(self, v):
122+
self.value = v
123+
124+
# Token value 50 is not > first_token (100) of the tablet whose
125+
# last_token (200) is >= 50, so no match.
126+
self.assertIsNone(tablets.get_tablet_for_key("ks", "tb", Token(50)))

0 commit comments

Comments
 (0)