Skip to content

perf: replace BytesIO with b"".join() in collection serialization#787

Draft
mykaul wants to merge 1 commit intoscylladb:masterfrom
mykaul:perf/collection-serialize-join
Draft

perf: replace BytesIO with b"".join() in collection serialization#787
mykaul wants to merge 1 commit intoscylladb:masterfrom
mykaul:perf/collection-serialize-join

Conversation

@mykaul
Copy link
Copy Markdown

@mykaul mykaul commented Apr 3, 2026

Motivation

The serialize_safe methods for collection types (ListType, SetType, MapType, TupleType, UserType) use io.BytesIO() as an intermediate buffer — creating a BytesIO object, writing fragments with multiple .write() calls, then extracting the result with .getvalue(). This pattern incurs overhead from:

  1. BytesIO object allocation on every serialization call
  2. Multiple method dispatch for each .write() call (Python method lookup + C-level buffer management)
  3. Repeated int32_pack(-1) calls for null elements, recomputing the same 4-byte value each time

Summary of Changes

  • Replace io.BytesIO() with list accumulation + b"".join() in four serialize_safe methods:
    • _SimpleParameterizedType.serialize_safe (used by ListType, SetType)
    • MapType.serialize_safe
    • TupleType.serialize_safe
    • UserType.serialize_safe
  • Add _INT32_NULL = int32_pack(-1) as a pre-computed module-level constant, eliminating repeated packing of the null sentinel value

The b"".join(parts) pattern is a well-known Python idiom that avoids intermediate buffer object overhead. CPython's bytes.join() pre-calculates the total output size and copies all fragments in a single pass, whereas BytesIO must manage a growable internal buffer with potential reallocations.

Note: PR #763 on this repo adds only the _INT32_NULL constant. This PR is a superset that also replaces BytesIO with b"".join() across all four collection/composite type serializers.

How It Was Tested

  • All unit tests pass: pytest tests/unit/test_types.py (62 passed), pytest tests/unit/test_query.py tests/unit/test_cluster.py (36 passed)
  • Correctness verified: before and after produce identical serialized bytes for all collection types

Benchmarks

Micro-benchmarks measuring end-to-end serialize_safe performance (Python 3.14, timeit):

Scenario Before (ns/call) After (ns/call) Speedup
List 10 elements 10790 8291 1.30x
List 100 elements 103234 60259 1.71x
Map 10 entries 19177 15654 1.23x
Map 100 entries 204483 113497 1.80x
Tuple 10 fields 18006 6795 2.65x
UDT 5 fields 5822 4579 1.27x

The pattern also produces simpler, more idiomatic Python code.

@mykaul mykaul force-pushed the perf/collection-serialize-join branch from d9b8b3c to 17369e1 Compare April 3, 2026 22:08
Sign up for free to 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.

1 participant