Skip to content

Commit 97cdad0

Browse files
committed
perf: add buffer fast-path for vector serializers
1 parent 84c53ec commit 97cdad0

2 files changed

Lines changed: 223 additions & 12 deletions

File tree

cassandra/serializers.pyx

Lines changed: 157 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -219,12 +219,7 @@ cdef class SerVectorType(Serializer):
219219
self.type_code = 0
220220

221221
cpdef bytes serialize(self, object value, int protocol_version):
222-
# Normalize to tuple/list so indexing works for any iterable.
223-
# The Python VectorType.serialize() only requires len() + iteration,
224-
# so we must accept the same inputs. Avoid a copy if value is
225-
# already a list or tuple (common fast path for embeddings).
226-
if not isinstance(value, (list, tuple)):
227-
value = tuple(value)
222+
cdef object result
228223
cdef Py_ssize_t v_length = len(value)
229224
if v_length != self.vector_size:
230225
raise ValueError(
@@ -233,6 +228,28 @@ cdef class SerVectorType(Serializer):
233228
self.vector_size, self.subtype.typename,
234229
self.vector_size, v_length))
235230

231+
if self.type_code == 1:
232+
result = self._serialize_float_buffer(value)
233+
if result is not None:
234+
return result
235+
elif self.type_code == 2:
236+
result = self._serialize_double_buffer(value)
237+
if result is not None:
238+
return result
239+
elif self.type_code == 3:
240+
result = self._serialize_int32_buffer(value)
241+
if result is not None:
242+
return result
243+
244+
# Keep indexable sequences on the fast path. Fall back to tuple()
245+
# only for iterable-only inputs so the Cython path still matches
246+
# Python VectorType.serialize() input semantics.
247+
if v_length != 0 and not isinstance(value, (list, tuple)):
248+
try:
249+
value[0]
250+
except (TypeError, KeyError, IndexError):
251+
value = tuple(value)
252+
236253
if self.type_code == 1:
237254
return self._serialize_float(value)
238255
elif self.type_code == 2:
@@ -245,8 +262,8 @@ cdef class SerVectorType(Serializer):
245262
cdef inline bytes _serialize_float(self, object values):
246263
"""Serialize a list of floats into a contiguous big-endian buffer.
247264
248-
``values`` is already a tuple (normalized in ``serialize()``), so
249-
indexing is always safe and fast.
265+
``values`` is already an indexable sequence (normalized in
266+
``serialize()``), so integer indexing is safe and fast.
250267
"""
251268
cdef Py_ssize_t i
252269
cdef Py_ssize_t buf_size = self.vector_size * 4
@@ -277,11 +294,54 @@ cdef class SerVectorType(Serializer):
277294

278295
return result
279296

297+
cdef inline object _serialize_float_buffer(self, object values):
298+
"""Fast path for contiguous float32 buffers (e.g. numpy float32 arrays).
299+
300+
No ``_check_float_range`` is needed: the typed memoryview
301+
``float[::1]`` constrains values to C float (IEEE 754 float32),
302+
so overflow is impossible by definition. Returns ``None`` when
303+
*values* does not support the buffer protocol with the required
304+
format, letting the caller fall through to the element-wise path.
305+
"""
306+
cdef float[::1] view
307+
cdef Py_ssize_t buf_size = self.vector_size * 4
308+
cdef Py_ssize_t i
309+
cdef object result
310+
cdef char *buf
311+
cdef char *dst
312+
cdef char *src
313+
cdef float val
314+
315+
try:
316+
view = values
317+
except (TypeError, ValueError):
318+
return None
319+
320+
if buf_size == 0:
321+
return b""
322+
323+
result = PyBytes_FromStringAndSize(NULL, buf_size)
324+
buf = PyBytes_AS_STRING(result)
325+
326+
if is_little_endian:
327+
for i in range(self.vector_size):
328+
val = view[i]
329+
src = <char *>&val
330+
dst = buf + i * 4
331+
dst[0] = src[3]
332+
dst[1] = src[2]
333+
dst[2] = src[1]
334+
dst[3] = src[0]
335+
else:
336+
memcpy(buf, &view[0], buf_size)
337+
338+
return result
339+
280340
cdef inline bytes _serialize_double(self, object values):
281341
"""Serialize a list of doubles into a contiguous big-endian buffer.
282342
283-
``values`` is already a tuple (normalized in ``serialize()``), so
284-
indexing is always safe and fast.
343+
``values`` is already an indexable sequence (normalized in
344+
``serialize()``), so integer indexing is safe and fast.
285345
"""
286346
cdef Py_ssize_t i
287347
cdef Py_ssize_t buf_size = self.vector_size * 8
@@ -313,11 +373,55 @@ cdef class SerVectorType(Serializer):
313373

314374
return result
315375

376+
cdef inline object _serialize_double_buffer(self, object values):
377+
"""Fast path for contiguous float64 buffers (e.g. numpy float64 arrays).
378+
379+
Returns ``None`` when *values* does not expose a compatible
380+
buffer, letting the caller fall through to the element-wise path.
381+
"""
382+
cdef double[::1] view
383+
cdef Py_ssize_t buf_size = self.vector_size * 8
384+
cdef Py_ssize_t i
385+
cdef object result
386+
cdef char *buf
387+
cdef char *dst
388+
cdef char *src
389+
cdef double val
390+
391+
try:
392+
view = values
393+
except (TypeError, ValueError):
394+
return None
395+
396+
if buf_size == 0:
397+
return b""
398+
399+
result = PyBytes_FromStringAndSize(NULL, buf_size)
400+
buf = PyBytes_AS_STRING(result)
401+
402+
if is_little_endian:
403+
for i in range(self.vector_size):
404+
val = view[i]
405+
src = <char *>&val
406+
dst = buf + i * 8
407+
dst[0] = src[7]
408+
dst[1] = src[6]
409+
dst[2] = src[5]
410+
dst[3] = src[4]
411+
dst[4] = src[3]
412+
dst[5] = src[2]
413+
dst[6] = src[1]
414+
dst[7] = src[0]
415+
else:
416+
memcpy(buf, &view[0], buf_size)
417+
418+
return result
419+
316420
cdef inline bytes _serialize_int32(self, object values):
317421
"""Serialize a list of int32 values into a contiguous big-endian buffer.
318422
319-
``values`` is already a tuple (normalized in ``serialize()``), so
320-
indexing is always safe and fast.
423+
``values`` is already an indexable sequence (normalized in
424+
``serialize()``), so integer indexing is safe and fast.
321425
"""
322426
cdef Py_ssize_t i
323427
cdef Py_ssize_t buf_size = self.vector_size * 4
@@ -348,6 +452,47 @@ cdef class SerVectorType(Serializer):
348452

349453
return result
350454

455+
cdef inline object _serialize_int32_buffer(self, object values):
456+
"""Fast path for contiguous int32 buffers (e.g. numpy int32 arrays).
457+
458+
No ``_coerce_int`` / ``_check_int32_range`` is needed: the typed
459+
memoryview ``int[::1]`` enforces 32-bit signed integer range at
460+
the buffer-protocol level. Returns ``None`` when *values* does
461+
not expose a compatible buffer, letting the caller fall through
462+
to the element-wise path.
463+
"""
464+
cdef int[::1] view
465+
cdef Py_ssize_t buf_size = self.vector_size * 4
466+
cdef Py_ssize_t i
467+
cdef object result
468+
cdef char *buf
469+
cdef char *dst
470+
cdef char *src
471+
472+
try:
473+
view = values
474+
except (TypeError, ValueError):
475+
return None
476+
477+
if buf_size == 0:
478+
return b""
479+
480+
result = PyBytes_FromStringAndSize(NULL, buf_size)
481+
buf = PyBytes_AS_STRING(result)
482+
483+
if is_little_endian:
484+
for i in range(self.vector_size):
485+
src = <char *>&view[i]
486+
dst = buf + i * 4
487+
dst[0] = src[3]
488+
dst[1] = src[2]
489+
dst[2] = src[1]
490+
dst[3] = src[0]
491+
else:
492+
memcpy(buf, &view[0], buf_size)
493+
494+
return result
495+
351496
cdef inline bytes _serialize_generic(self, object values, int protocol_version):
352497
"""Fallback: element-by-element Python serialization for non-optimized types."""
353498
import io

tests/unit/cython/test_serializers.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import struct
2424
import unittest
2525

26+
import numpy as np
27+
2628
from tests.unit.cython.utils import cythontest
2729

2830
from cassandra.cython_deps import HAVE_CYTHON
@@ -534,6 +536,70 @@ def test_tuple_input(self):
534536
python_bytes = vec_type.serialize([1, 2, 3], PROTO)
535537
self.assertEqual(cython_bytes, python_bytes)
536538

539+
def test_indexable_sequence_input(self):
540+
"""Indexable non-list sequences should avoid tuple normalization."""
541+
542+
class IndexableSequence:
543+
def __init__(self, data):
544+
self._data = list(data)
545+
546+
def __len__(self):
547+
return len(self._data)
548+
549+
def __getitem__(self, index):
550+
return self._data[index]
551+
552+
def __iter__(self):
553+
return iter(self._data)
554+
555+
vec_type = _make_vector_type(FloatType, 3)
556+
ser = SerVectorType(vec_type)
557+
values = IndexableSequence([1.0, 2.0, 3.0])
558+
cython_bytes = ser.serialize(values, PROTO)
559+
python_bytes = vec_type.serialize([1.0, 2.0, 3.0], PROTO)
560+
self.assertEqual(cython_bytes, python_bytes)
561+
562+
def test_numpy_float32_array_input(self):
563+
"""NumPy float32 arrays should hit the float buffer fast path."""
564+
vec_type = _make_vector_type(FloatType, 4)
565+
ser = SerVectorType(vec_type)
566+
values = np.asarray([1.0, 2.0, 3.0, 4.0], dtype=np.float32)
567+
cython_bytes = ser.serialize(values, PROTO)
568+
python_bytes = vec_type.serialize(values, PROTO)
569+
self.assertEqual(cython_bytes, python_bytes)
570+
571+
def test_numpy_float64_array_input(self):
572+
"""NumPy float64 arrays should hit the double buffer fast path."""
573+
vec_type = _make_vector_type(DoubleType, 3)
574+
ser = SerVectorType(vec_type)
575+
values = np.asarray([1.0, -2.5, 3.14], dtype=np.float64)
576+
cython_bytes = ser.serialize(values, PROTO)
577+
python_bytes = vec_type.serialize(values, PROTO)
578+
self.assertEqual(cython_bytes, python_bytes)
579+
580+
def test_numpy_int32_array_input(self):
581+
"""NumPy int32 arrays should hit the int32 buffer fast path."""
582+
vec_type = _make_vector_type(Int32Type, 3)
583+
ser = SerVectorType(vec_type)
584+
values = np.asarray([2147483647, -2147483648, 0], dtype=np.int32)
585+
cython_bytes = ser.serialize(values, PROTO)
586+
python_bytes = vec_type.serialize(values, PROTO)
587+
self.assertEqual(cython_bytes, python_bytes)
588+
589+
def test_numpy_dtype_mismatch_fallthrough(self):
590+
"""dtype mismatch should fall through to element-wise path correctly.
591+
592+
A float64 array passed to a FloatType vector cannot bind to
593+
float[::1], so the serializer must fall through to the
594+
element-wise path and still produce correct bytes.
595+
"""
596+
vec_type = _make_vector_type(FloatType, 3)
597+
ser = SerVectorType(vec_type)
598+
values = np.asarray([1.0, 2.0, 3.0], dtype=np.float64)
599+
cython_bytes = ser.serialize(values, PROTO)
600+
python_bytes = vec_type.serialize(values, PROTO)
601+
self.assertEqual(cython_bytes, python_bytes)
602+
537603

538604
# ---------------------------------------------------------------------------
539605
# Factory function tests

0 commit comments

Comments
 (0)