Skip to content

Commit 88833fc

Browse files
committed
perf: replace BytesIO with b''.join() in collection serialization
1 parent b8b714c commit 88833fc

2 files changed

Lines changed: 178 additions & 25 deletions

File tree

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
#!/usr/bin/env python
2+
# Copyright ScyllaDB, Inc.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
"""
17+
Benchmark: collection serialization - BytesIO vs b"".join(parts).
18+
19+
Measures end-to-end serialize_safe performance for List, Map, Tuple, and UserType
20+
with varying collection sizes.
21+
"""
22+
23+
import timeit
24+
import sys
25+
import os
26+
27+
# Add project root to path
28+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
29+
30+
from cassandra.cqltypes import (
31+
ListType,
32+
SetType,
33+
MapType,
34+
TupleType,
35+
UserType,
36+
Int32Type,
37+
UTF8Type,
38+
FloatType,
39+
)
40+
41+
PROTOCOL_VERSION = 4
42+
43+
# Build parameterized types
44+
ListOfInt = ListType.apply_parameters([Int32Type])
45+
SetOfInt = SetType.apply_parameters([Int32Type])
46+
MapIntToStr = MapType.apply_parameters([Int32Type, UTF8Type])
47+
48+
49+
# For TupleType and UserType, we need to set subtypes on a subclass
50+
class TestTupleType(TupleType):
51+
subtypes = (
52+
Int32Type,
53+
Int32Type,
54+
Int32Type,
55+
Int32Type,
56+
Int32Type,
57+
Int32Type,
58+
Int32Type,
59+
Int32Type,
60+
Int32Type,
61+
Int32Type,
62+
)
63+
64+
65+
class TestUserType(UserType):
66+
subtypes = (Int32Type, UTF8Type, FloatType, Int32Type, UTF8Type)
67+
fieldnames = ("id", "name", "score", "age", "email")
68+
typename = "test_udt"
69+
keyspace = "test_ks"
70+
mapped_class = None
71+
tuple_type = None
72+
73+
74+
def run_bench(label, fn, args, n):
75+
# Warm up
76+
for _ in range(min(1000, n)):
77+
fn(*args)
78+
t = timeit.timeit(lambda: fn(*args), number=n)
79+
ns_per_call = t / n * 1e9
80+
print(f" {label:45s} {t:.3f}s ({ns_per_call:.2f} ns/call)")
81+
return t, ns_per_call
82+
83+
84+
# Test data
85+
list_10 = list(range(10))
86+
list_100 = list(range(100))
87+
list_1000 = list(range(1000))
88+
list_with_nulls = [i if i % 3 != 0 else None for i in range(100)]
89+
90+
map_10 = {i: f"value_{i}" for i in range(10)}
91+
map_100 = {i: f"value_{i}" for i in range(100)}
92+
93+
tuple_10 = tuple(range(10))
94+
udt_val = (1, "test_name", 3.14, 25, "test@example.com")
95+
96+
N_SMALL = 500_000
97+
N_MED = 100_000
98+
N_LARGE = 10_000
99+
100+
print("Collection serialization benchmark")
101+
print("=" * 70)
102+
103+
results = {}
104+
105+
print("\nListType.serialize (list of int32):")
106+
_, r = run_bench(
107+
"10 elements", ListOfInt.serialize, (list_10, PROTOCOL_VERSION), N_SMALL
108+
)
109+
results["list_10"] = r
110+
_, r = run_bench(
111+
"100 elements", ListOfInt.serialize, (list_100, PROTOCOL_VERSION), N_MED
112+
)
113+
results["list_100"] = r
114+
_, r = run_bench(
115+
"1000 elements", ListOfInt.serialize, (list_1000, PROTOCOL_VERSION), N_LARGE
116+
)
117+
results["list_1000"] = r
118+
_, r = run_bench(
119+
"100 elements (33% null)",
120+
ListOfInt.serialize,
121+
(list_with_nulls, PROTOCOL_VERSION),
122+
N_MED,
123+
)
124+
results["list_100_nulls"] = r
125+
126+
print("\nMapType.serialize (map<int32, text>):")
127+
_, r = run_bench(
128+
"10 entries", MapIntToStr.serialize, (map_10, PROTOCOL_VERSION), N_SMALL
129+
)
130+
results["map_10"] = r
131+
_, r = run_bench(
132+
"100 entries", MapIntToStr.serialize, (map_100, PROTOCOL_VERSION), N_MED
133+
)
134+
results["map_100"] = r
135+
136+
print("\nTupleType.serialize (10 x int32):")
137+
_, r = run_bench(
138+
"10 fields", TestTupleType.serialize, (tuple_10, PROTOCOL_VERSION), N_SMALL
139+
)
140+
results["tuple_10"] = r
141+
142+
print("\nUserType.serialize (5 fields: int, text, float, int, text):")
143+
_, r = run_bench(
144+
"5 fields", TestUserType.serialize, (udt_val, PROTOCOL_VERSION), N_SMALL
145+
)
146+
results["udt_5"] = r
147+
148+
# Print summary for easy comparison
149+
print(f"\n{'=' * 70}")
150+
print("Summary (ns/call):")
151+
for k, v in results.items():
152+
print(f" {k:25s}: {v:.2f} ns")

cassandra/cqltypes.py

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@
6363

6464
_number_types = frozenset((int, float))
6565

66+
# Pre-computed null sentinel for collection element serialization (int32 -1)
67+
_INT32_NULL = int32_pack(-1)
68+
6669

6770
def _name_from_hex_string(encoded_name):
6871
bin_str = unhexlify(encoded_name)
@@ -836,17 +839,16 @@ def serialize_safe(cls, items, protocol_version):
836839
raise TypeError("Received a string for a type that expects a sequence")
837840

838841
subtype, = cls.subtypes
839-
buf = io.BytesIO()
840-
buf.write(int32_pack(len(items)))
841842
inner_proto = max(3, protocol_version)
843+
parts = [int32_pack(len(items))]
842844
for item in items:
843845
if item is None:
844-
buf.write(int32_pack(-1))
846+
parts.append(_INT32_NULL)
845847
else:
846848
itembytes = subtype.to_binary(item, inner_proto)
847-
buf.write(int32_pack(len(itembytes)))
848-
buf.write(itembytes)
849-
return buf.getvalue()
849+
parts.append(int32_pack(len(itembytes)))
850+
parts.append(itembytes)
851+
return b"".join(parts)
850852

851853

852854
class ListType(_SimpleParameterizedType):
@@ -899,27 +901,26 @@ def deserialize_safe(cls, byts, protocol_version):
899901
@classmethod
900902
def serialize_safe(cls, themap, protocol_version):
901903
key_type, value_type = cls.subtypes
902-
buf = io.BytesIO()
903-
buf.write(int32_pack(len(themap)))
904904
try:
905905
items = themap.items()
906906
except AttributeError:
907907
raise TypeError("Got a non-map object for a map value")
908908
inner_proto = max(3, protocol_version)
909+
parts = [int32_pack(len(themap))]
909910
for key, val in items:
910911
if key is not None:
911912
keybytes = key_type.to_binary(key, inner_proto)
912-
buf.write(int32_pack(len(keybytes)))
913-
buf.write(keybytes)
913+
parts.append(int32_pack(len(keybytes)))
914+
parts.append(keybytes)
914915
else:
915-
buf.write(int32_pack(-1))
916+
parts.append(_INT32_NULL)
916917
if val is not None:
917918
valbytes = value_type.to_binary(val, inner_proto)
918-
buf.write(int32_pack(len(valbytes)))
919-
buf.write(valbytes)
919+
parts.append(int32_pack(len(valbytes)))
920+
parts.append(valbytes)
920921
else:
921-
buf.write(int32_pack(-1))
922-
return buf.getvalue()
922+
parts.append(_INT32_NULL)
923+
return b"".join(parts)
923924

924925

925926
class TupleType(_ParameterizedType):
@@ -957,15 +958,15 @@ def serialize_safe(cls, val, protocol_version):
957958
(len(cls.subtypes), len(val), val))
958959

959960
proto_version = max(3, protocol_version)
960-
buf = io.BytesIO()
961+
parts = []
961962
for item, subtype in zip(val, cls.subtypes):
962963
if item is not None:
963964
packed_item = subtype.to_binary(item, proto_version)
964-
buf.write(int32_pack(len(packed_item)))
965-
buf.write(packed_item)
965+
parts.append(int32_pack(len(packed_item)))
966+
parts.append(packed_item)
966967
else:
967-
buf.write(int32_pack(-1))
968-
return buf.getvalue()
968+
parts.append(_INT32_NULL)
969+
return b"".join(parts)
969970

970971
@classmethod
971972
def cql_parameterized_type(cls):
@@ -1026,7 +1027,7 @@ def deserialize_safe(cls, byts, protocol_version):
10261027
@classmethod
10271028
def serialize_safe(cls, val, protocol_version):
10281029
proto_version = max(3, protocol_version)
1029-
buf = io.BytesIO()
1030+
parts = []
10301031
for i, (fieldname, subtype) in enumerate(zip(cls.fieldnames, cls.subtypes)):
10311032
# first treat as a tuple, else by custom type
10321033
try:
@@ -1038,11 +1039,11 @@ def serialize_safe(cls, val, protocol_version):
10381039

10391040
if item is not None:
10401041
packed_item = subtype.to_binary(item, proto_version)
1041-
buf.write(int32_pack(len(packed_item)))
1042-
buf.write(packed_item)
1042+
parts.append(int32_pack(len(packed_item)))
1043+
parts.append(packed_item)
10431044
else:
1044-
buf.write(int32_pack(-1))
1045-
return buf.getvalue()
1045+
parts.append(_INT32_NULL)
1046+
return b"".join(parts)
10461047

10471048
@classmethod
10481049
def _make_registered_udt_namedtuple(cls, keyspace, name, field_names):

0 commit comments

Comments
 (0)