perf: buffer accumulation in _write_query_params() reduces f.write() calls (~100ns improvement, 1.1-1.3x speedup) - #790
Conversation
bc1545f to
9b21d5b
Compare
|
Just spitting this here: Honest answer: on its own, ~100ns per call is tiny. But context matters:
|
f2be2a8 to
ac64459
Compare
ac64459 to
1e1e709
Compare
Replace the per-parameter write_value(f, param) loop in _QueryMessage._write_query_params() with a buffer accumulation approach: list.append + b"".join + single f.write(). This reduces the number of f.write() calls from 2*N+1 to 1, which is significant for vector workloads with large parameters. Also removes the redundant ExecuteMessage._write_query_params() pass-through override to avoid extra MRO lookup per call. Includes 14 unit tests covering normal, NULL, UNSET, empty, large vector, and mixed parameter scenarios for both ExecuteMessage and QueryMessage. Includes a benchmark script (benchmarks/bench_execute_write_params.py).
1e1e709 to
9a41e1f
Compare
|
Warning Review limit reached
Next review available in: 55 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: QUIET Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
Comment |
There was a problem hiding this comment.
Pull request overview
Note
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Optimizes query-parameter serialization in protocol messages by buffering encoded parameter bytes and writing them in a single f.write(), reducing per-parameter write calls in hot paths.
Changes:
- Buffer-accumulate query parameter bytes in
_QueryMessage._write_query_params()and write once. - Remove redundant
ExecuteMessage._write_query_params()pass-through override. - Add unit tests validating serialization parity and add a local benchmarking script.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| cassandra/protocol.py | Implements buffered parameter encoding and removes redundant override to reduce overhead in execute/query paths. |
| tests/unit/test_protocol.py | Adds unit tests to validate the new buffered encoding matches prior write_value() behavior. |
| benchmarks/bench_execute_write_params.py | Adds a standalone benchmark script to compare encoding variants and measure performance. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Accumulate param bytes in a list and write once instead of | ||
| # 2*N+1 separate f.write() calls via write_value(). | ||
| _int32_pack = int32_pack | ||
| parts = [uint16_pack(len(self.query_params))] | ||
| _parts_append = parts.append | ||
| for param in self.query_params: | ||
| write_value(f, param) | ||
| if param is None: | ||
| _parts_append(_int32_pack(-1)) | ||
| elif param is _UNSET_VALUE: | ||
| _parts_append(_int32_pack(-2)) | ||
| else: | ||
| _parts_append(_int32_pack(len(param))) | ||
| _parts_append(param) | ||
| f.write(b"".join(parts)) |
| raw = self._execute_msg_bytes(msg, protocol_version=4) | ||
| self.assertIn(expected, raw) |
| def test_none_query_params(self): | ||
| """When query_params is None, no param block should be written.""" | ||
| msg1 = ExecuteMessage(query_id=b'qid', query_params=None, | ||
| consistency_level=1) | ||
| msg2 = ExecuteMessage(query_id=b'qid', query_params=[b'x'], | ||
| consistency_level=1) | ||
| raw1 = self._execute_msg_bytes(msg1, protocol_version=4) | ||
| raw2 = self._execute_msg_bytes(msg2, protocol_version=4) | ||
| # raw1 should be shorter (no param section) | ||
| self.assertLess(len(raw1), len(raw2)) |
| # Accumulate param bytes in a list and write once instead of | ||
| # 2*N+1 separate f.write() calls via write_value(). |
Summary
Replace per-parameter
write_value(f, param)loops with buffer accumulation (list.append+b"".join+ singlef.write()), reducingf.write()calls from(2*N + 1)to 1 for N query parameters in the execute/query path.What changed
cassandra/protocol.py_QueryMessage._write_query_params()-- Buffer accumulation for the parameter loop. Local variable caching (_int32_pack,_parts_append) for Cython-friendly tight loop.ExecuteMessage._write_query_params()-- Removed unnecessarysuper()pass-through override (now inherited directly from_QueryMessage).tests/unit/test_protocol.pyAdded 14 new test methods in
WriteQueryParamsBufferAccumulationTest.Benchmark
Measured with
min()oftimeit.repeat(repeat=7, number=200_000)on a quiet machine (load <3), Cython.socompiled, before/after rebuild.For single-param workloads, the list/join overhead slightly exceeds the write-call savings. The benefit appears with multiple params (10+), where reducing
2*Nwrites to 1 join becomes significant. The real payoff is inBatchMessage.send_body()(PR #791), where N is much larger.Tests