Skip to content

perf: use stdlib bisect and attrgetter in tablets.py (100's of ns, 1.5-5.6x speedup) - #757

Merged
Lorak-mmk merged 1 commit into
scylladb:masterfrom
mykaul:perf/tablets-stdlib-bisect
Apr 9, 2026
Merged

perf: use stdlib bisect and attrgetter in tablets.py (100's of ns, 1.5-5.6x speedup)#757
Lorak-mmk merged 1 commit into
scylladb:masterfrom
mykaul:perf/tablets-stdlib-bisect

Conversation

@mykaul

@mykaulmykaul commented Mar 20, 2026

Copy link
Copy Markdown

Summary

  • Use stdlib bisect.bisect_left unconditionally (C implementation). Since we only support Python 3.10-3.14, drop the bundled pure-Python fallback entirely.
  • Replace per-call lambda closures with module-level operator.attrgetter for first_token / last_token extraction, avoiding repeated function-object allocation.
  • Add unit tests for get_tablet_for_key (3 tests).

Benchmark Results

Measured on Intel i7-1270P, Python 3.14.3, CPU-pinned, 200k iterations.

get_tablet_for_key (hit — the primary hot path)

TabletsBefore (ns)After (ns)Speedup
103592401.50x
1005152961.74x
1,0008234052.03x
10,0001,0494502.33x

bisect_left with key= (isolated)

SizeBefore (ns)After (ns)Speedup
102621202.18x
1003811662.30x
1,0006072762.20x
10,0008483422.48x

bisect_left without key= (plain ints)

SizeBefore (ns)After (ns)Speedup
10164592.78x
1002551242.06x
1,0004741034.60x
10,0006281115.66x

@Lorak-mmk
Lorak-mmk self-requested a review March 20, 2026 19:09
@mykaul
mykaulforce-pushed the perf/tablets-stdlib-bisect branch from 75eaf9b to dd449caCompareApril 1, 2026 20:02
@Lorak-mmk

Copy link
Copy Markdown

According to our README, we support Python 3.10-3.14 right now, so it should be fine to drop the pure-python impl and use the builtin one instead.

@mykaulmykaul changed the title perf: use stdlib bisect and attrgetter in tablets.pyperf: use stdlib bisect and attrgetter in tablets.py (100's of ns, 1.4-6.x speedup)Apr 7, 2026
- 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)
@mykaul
mykaulforce-pushed the perf/tablets-stdlib-bisect branch from dd449ca to 4ba3f2bCompareApril 9, 2026 07:55
@mykaulmykaul changed the title perf: use stdlib bisect and attrgetter in tablets.py (100's of ns, 1.4-6.x speedup)perf: use stdlib bisect and attrgetter in tablets.py (100's of ns, 1.5-5.6x speedup)Apr 9, 2026
@mykaul

Copy link
Copy Markdown
Author

According to our README, we support Python 3.10-3.14 right now, so it should be fine to drop the pure-python impl and use the builtin one instead.

Removed.

@Lorak-mmk
Lorak-mmk merged commit 5094118 into scylladb:masterApr 9, 2026
11 of 13 checks passed
mykaul added a commit to mykaul/python-driver that referenced this pull request Apr 9, 2026
Maintain parallel _first_tokens and _last_tokens dicts alongside
_tablets, each mapping (keyspace, table) to a plain list[int]. This
lets bisect_left run entirely in C on native ints instead of calling
an attrgetter callback on every comparison during binary search.
Follow-up to PR scylladb#757 which identified the opportunity: its own
benchmarks showed bisect_left without key= is 2.7-5.7x faster than
with key=attrgetter.
Results (best-of-5, Python 3.14):
get_tablet_for_key (hit):
Tablets Before After Saved Speedup
10 293ns 216ns 78ns 1.36x
100 351ns 233ns 118ns 1.51x
1,000 448ns 267ns 181ns 1.68x
10,000 537ns 282ns 255ns 1.90x
All three dicts are kept in sync by add_tablet, drop_tablets, and
drop_tablets_by_host_id. The attrgetter imports are no longer needed
and have been removed.
mykaul added a commit to mykaul/python-driver that referenced this pull request Apr 21, 2026
Maintain parallel _first_tokens and _last_tokens dicts alongside
_tablets, each mapping (keyspace, table) to a plain list[int]. This
lets bisect_left run entirely in C on native ints instead of calling
an attrgetter callback on every comparison during binary search.
Follow-up to PR scylladb#757 which identified the opportunity: its own
benchmarks showed bisect_left without key= is 2.7-5.7x faster than
with key=attrgetter.
Results (best-of-5, Python 3.14):
get_tablet_for_key (hit):
Tablets Before After Saved Speedup
10 293ns 216ns 78ns 1.36x
100 351ns 233ns 118ns 1.51x
1,000 448ns 267ns 181ns 1.68x
10,000 537ns 282ns 255ns 1.90x
All three dicts are kept in sync by add_tablet, drop_tablets, and
drop_tablets_by_host_id. The attrgetter imports are no longer needed
and have been removed.
mykaul added a commit to mykaul/python-driver that referenced this pull request Jun 29, 2026
Maintain parallel _first_tokens and _last_tokens dicts alongside
_tablets, each mapping (keyspace, table) to a plain list[int]. This
lets bisect_left run entirely in C on native ints instead of calling
an attrgetter callback on every comparison during binary search.
Follow-up to PR scylladb#757 which identified the opportunity: its own
benchmarks showed bisect_left without key= is 2.7-5.7x faster than
with key=attrgetter.
Results (best-of-5, Python 3.14):
get_tablet_for_key (hit):
Tablets Before After Saved Speedup
10 293ns 216ns 78ns 1.36x
100 351ns 233ns 118ns 1.51x
1,000 448ns 267ns 181ns 1.68x
10,000 537ns 282ns 255ns 1.90x
All three dicts are kept in sync by add_tablet, drop_tablets, and
drop_tablets_by_host_id. The attrgetter imports are no longer needed
and have been removed.
mykaul added a commit to mykaul/python-driver that referenced this pull request Jun 30, 2026
Maintain parallel _first_tokens and _last_tokens dicts alongside
_tablets, each mapping (keyspace, table) to a plain list[int]. This
lets bisect_left run entirely in C on native ints instead of calling
an attrgetter callback on every comparison during binary search.
Follow-up to PR scylladb#757 which identified the opportunity: its own
benchmarks showed bisect_left without key= is 2.7-5.7x faster than
with key=attrgetter.
Results (best-of-5, Python 3.14):
get_tablet_for_key (hit):
Tablets Before After Saved Speedup
10 293ns 216ns 78ns 1.36x
100 351ns 233ns 118ns 1.51x
1,000 448ns 267ns 181ns 1.68x
10,000 537ns 282ns 255ns 1.90x
All three dicts are kept in sync by add_tablet, drop_tablets, and
drop_tablets_by_host_id. The attrgetter imports are no longer needed
and have been removed.
mykaul added a commit to mykaul/python-driver that referenced this pull request Jul 29, 2026
Maintain parallel _first_tokens and _last_tokens dicts alongside
_tablets, each mapping (keyspace, table) to a plain list[int]. This
lets bisect_left run entirely in C on native ints instead of calling
an attrgetter callback on every comparison during binary search.
Follow-up to PR scylladb#757 which identified the opportunity: its own
benchmarks showed bisect_left without key= is 2.7-5.7x faster than
with key=attrgetter.
Results (best-of-5, Python 3.14):
get_tablet_for_key (hit):
Tablets Before After Saved Speedup
10 293ns 216ns 78ns 1.36x
100 351ns 233ns 118ns 1.51x
1,000 448ns 267ns 181ns 1.68x
10,000 537ns 282ns 255ns 1.90x
All three dicts are kept in sync by add_tablet, drop_tablets, and
drop_tablets_by_host_id. The attrgetter imports are no longer needed
and have been removed.
mykaul added a commit to mykaul/python-driver that referenced this pull request Jul 31, 2026
Maintain parallel _first_tokens and _last_tokens dicts alongside
_tablets, each mapping (keyspace, table) to a plain list[int]. This
lets bisect_left run entirely in C on native ints instead of calling
an attrgetter callback on every comparison during binary search.
Follow-up to PR scylladb#757 which identified the opportunity: its own
benchmarks showed bisect_left without key= is 2.7-5.7x faster than
with key=attrgetter.
Results (best-of-5, Python 3.14):
get_tablet_for_key (hit):
Tablets Before After Saved Speedup
10 293ns 216ns 78ns 1.36x
100 351ns 233ns 118ns 1.51x
1,000 448ns 267ns 181ns 1.68x
10,000 537ns 282ns 255ns 1.90x
All three dicts are kept in sync by add_tablet, drop_tablets, and
drop_tablets_by_host_id. The attrgetter imports are no longer needed
and have been removed.
Also drop the mutable {} class-level defaults this same change added
(_first_tokens, _last_tokens), plus the pre-existing _tablets = {} they
were modeled on: leaving mutable dicts at class scope is a latent
shared-state hazard (e.g. if a future alternative constructor bypassed
__init__), even though __init__ already reassigns them per instance
today. All three are now instance-only, initialized solely in
__init__. Add TabletsInstanceStateTest to lock this in: one check that
the class body itself carries no such attributes, one check that two
instances never observe each other's state.
mykaul added a commit to mykaul/python-driver that referenced this pull request Aug 14, 2026
Maintain parallel _first_tokens and _last_tokens dicts alongside
_tablets, each mapping (keyspace, table) to a plain list[int]. This
lets bisect_left run entirely in C on native ints instead of calling
an attrgetter callback on every comparison during binary search.
Follow-up to PR scylladb#757 which identified the opportunity: its own
benchmarks showed bisect_left without key= is 2.7-5.7x faster than
with key=attrgetter.
Results (best-of-5, Python 3.14):
get_tablet_for_key (hit):
Tablets Before After Saved Speedup
10 293ns 216ns 78ns 1.36x
100 351ns 233ns 118ns 1.51x
1,000 448ns 267ns 181ns 1.68x
10,000 537ns 282ns 255ns 1.90x
All three dicts are kept in sync by add_tablet, drop_tablets, and
drop_tablets_by_host_id. The attrgetter imports are no longer needed
and have been removed.
Also drop the mutable {} class-level defaults this same change added
(_first_tokens, _last_tokens), plus the pre-existing _tablets = {} they
were modeled on: leaving mutable dicts at class scope is a latent
shared-state hazard (e.g. if a future alternative constructor bypassed
__init__), even though __init__ already reassigns them per instance
today. All three are now instance-only, initialized solely in
__init__. Add TabletsInstanceStateTest to lock this in: one check that
the class body itself carries no such attributes, one check that two
instances never observe each other's state.
Signed-off-by: Yaniv Kaul <yaniv.kaul@scylladb.com>
Sign up for freeto 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.

2 participants

@mykaul@Lorak-mmk