Skip to content

[DO NOT MERGE] (Improvement) improve performance of Vector type parsing - #689

Draft
mykaul wants to merge 17 commits into
scylladb:masterfrom
mykaul:int32_pack
Draft

[DO NOT MERGE] (Improvement) improve performance of Vector type parsing#689
mykaul wants to merge 17 commits into
scylladb:masterfrom
mykaul:int32_pack

Conversation

@mykaul

@mykaul mykaul commented Feb 5, 2026

Copy link
Copy Markdown

Multiple partially/mostly independent commits (if needed, most can be extracted from this series) to improve the parsing of vector arrays.
Across Python, Cython and even Numpy array creation, this series includes both test and a benchmark to improve the deserialization of vectors.

There's some prerequisite and a bug fix (that I've extracted to its own PR), but otherwise the series is mostly complete.

I think in a follow-up or following commits I'll add the same/similar to serialization of vector types.

Pre-review checklist

  • I have split my patch into logically separate commits.
  • All commit messages clearly explain what they change and why.
  • I added relevant tests for new features and bug fixes.
  • All commits compile, pass static checks and pass test.
  • PR description sums up the changes and reasons why they should be introduced.
  • I have provided docstrings for the public items that I want to introduce.
  • I have adjusted the documentation in ./docs/source/.
  • I added appropriate Fixes: annotations to PR description.

@mykaul
mykaul requested a review from Copilot February 5, 2026 21:46
@mykaul mykaul added the enhancement New feature or request label Feb 5, 2026
@mykaul
mykaul marked this pull request as draft February 5, 2026 21:52

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request implements comprehensive performance optimizations for VectorType deserialization across multiple layers of the Python driver stack. The changes introduce optimized deserialization paths using struct.unpack for small vectors, numpy.frombuffer for large vectors (when NumPy is available), and a new Cython DesVectorType deserializer that uses low-level C operations with ntohl/ntohs intrinsics for efficient byte-swapping.

Changes:

  • Added Cython-based DesVectorType deserializer with optimized paths for float, double, int32, int64, and int16 vector types
  • Enhanced Python-level VectorType deserialization with struct.unpack and numpy.frombuffer optimizations
  • Extended NumpyParser to create 2D arrays for vector types, enabling efficient batch processing
  • Optimized low-level byte-swap operations using ntohl/ntohs intrinsics and simplified varint_unpack using int.from_bytes
  • Removed slice_buffer function in favor of simpler from_ptr_and_size for direct pointer manipulation

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
tests/unit/test_types.py Adds comprehensive tests for Cython DesVectorType deserializer covering float, double, int32, int64, and int16 vectors
tests/unit/test_numpy_parser.py New test suite for NumPy parser 2D array support for vectors with mixed column types and large dimensions
cassandra/cqltypes.py Python-level VectorType optimizations using struct.unpack and numpy.frombuffer, plus improved variable-size vector handling
cassandra/deserializers.pyx New Cython DesVectorType class with type-specific optimized deserialization methods
cassandra/numpy_parser.pyx Enhanced to create 2D NumPy arrays for VectorType columns and pre-allocate arrays list
cassandra/cython_marshal.pyx Optimized unpack_num to use ntohl/ntohs intrinsics and simplified varint_unpack
cassandra/ioutils.pyx Optimized read_int to use ntohl directly
cassandra/marshal.py Simplified varint_unpack using int.from_bytes
cassandra/buffer.pxd Replaced slice_buffer with simpler from_ptr_and_size function
benchmarks/vector_deserialize.py New comprehensive benchmark suite comparing different optimization strategies

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread cassandra/deserializers.pyx Outdated
Comment thread cassandra/cython_marshal.pyx Outdated
Comment thread cassandra/ioutils.pyx Outdated
Comment thread benchmarks/vector_deserialize.py Outdated
Comment thread benchmarks/vector_deserialize.py Outdated
Comment thread benchmarks/vector_deserialize.py Outdated
Comment thread cassandra/deserializers.pyx Outdated
Comment thread cassandra/deserializers.pyx Outdated
Comment thread cassandra/deserializers.pyx Outdated
Comment thread cassandra/deserializers.pyx
@mykaul

mykaul commented Feb 9, 2026

Copy link
Copy Markdown
Author

Per discussion earlier, Iv'e changed to optimize only for float/double/int, since those are the more frequently used and are not troublesome protocol-wise.
Results:

Benchmark	Master (μs)	int32_pack (μs)	Change (μs)	Change (%)
Vector<float, 3>	3.32	1.34	-1.98	-59.64%
Vector<float, 4>	2.21	0.78	-1.43	-64.71%
Vector<float, 128>	46.78	3.53	-43.25	-92.45%
Vector<float, 384>	145.53	10.12	-135.41	-93.05%
Vector<float, 768>	287.93	19.20	-268.73	-93.33%
Vector<float, 1536>	579.66	37.84	-541.82	-93.48%
Vector<double, 128>	47.88	3.80	-44.08	-92.07%
Vector<double, 768>	294.95	19.72	-275.23	-93.31%
Vector<double, 1536>	584.74	37.69	-547.05	-93.55%
Vector<int, 64>	22.06	2.19	-19.87	-90.07%
Vector<int, 128>	43.50	2.82	-40.68	-93.52%

I'll submit a fixed version of this series.

@mykaul

mykaul commented Mar 2, 2026

Copy link
Copy Markdown
Author

Improvements in μs are not worth pursuing right now. Perhaps in the future, I'll extract some parts of it.

@mykaul
mykaul force-pushed the int32_pack branch 5 times, most recently from 866984c to 7829b6c Compare March 6, 2026 15:57
@mykaul mykaul changed the title (Improvement) improve performance of Vector type parsing [DO NOT MERGE] (Improvement) improve performance of Vector type parsing Apr 2, 2026
@mykaul

mykaul commented Apr 2, 2026

Copy link
Copy Markdown
Author

mykaul added a commit to mykaul/python-driver that referenced this pull request Apr 5, 2026
…bytes

Replace the manual string-formatting hex conversion in varint_unpack()
and the byte-by-byte bytearray loop in varint_pack() with Python 3
builtins int.from_bytes() and int.to_bytes().

varint_unpack used '%02x' formatting per byte, str.join, then
int(..., 16) to parse back — O(n) string allocations.  int.from_bytes
is a single C-level call.

varint_pack used a while loop appending individual bytes to a bytearray,
then reversing.  int.to_bytes computes the result in one C call.

Also fixes the Cython path in cython_marshal.pyx which had the same
slow pattern with a TODO comment to optimize.

Adapted from PR scylladb#689 (varint_unpack) with new varint_pack implementation.

varint_pack  medium:   643 ->  90 ns/call  (7.1x faster)
varint_pack  large:   1109 ->  96 ns/call (11.6x faster)
varint_unpack medium: 1086 -> 115 ns/call  (9.4x faster)
varint_unpack large:  1940 -> 146 ns/call (13.3x faster)
mykaul added a commit to mykaul/python-driver that referenced this pull request Apr 11, 2026
…bytes

Replace the manual string-formatting hex conversion in varint_unpack()
and the byte-by-byte bytearray loop in varint_pack() with Python 3
builtins int.from_bytes() and int.to_bytes().

varint_unpack used '%02x' formatting per byte, str.join, then
int(..., 16) to parse back — O(n) string allocations.  int.from_bytes
is a single C-level call.

varint_pack used a while loop appending individual bytes to a bytearray,
then reversing.  int.to_bytes computes the result in one C call.

Also fixes the Cython path in cython_marshal.pyx which had the same
slow pattern with a TODO comment to optimize.

Adapted from PR scylladb#689 (varint_unpack) with new varint_pack implementation.

varint_pack  medium:   643 ->  90 ns/call  (7.1x faster)
varint_pack  large:   1109 ->  96 ns/call (11.6x faster)
varint_unpack medium: 1086 -> 115 ns/call  (9.4x faster)
varint_unpack large:  1940 -> 146 ns/call (13.3x faster)
mykaul added a commit to mykaul/python-driver that referenced this pull request Apr 11, 2026
…bytes

Replace the manual string-formatting hex conversion in varint_unpack()
and the byte-by-byte bytearray loop in varint_pack() with Python 3
builtins int.from_bytes() and int.to_bytes().

varint_unpack used '%02x' formatting per byte, str.join, then
int(..., 16) to parse back — O(n) string allocations.  int.from_bytes
is a single C-level call.

varint_pack used a while loop appending individual bytes to a bytearray,
then reversing.  int.to_bytes computes the result in one C call.

Also fixes the Cython path in cython_marshal.pyx which had the same
slow pattern with a TODO comment to optimize.

Adapted from PR scylladb#689 (varint_unpack) with new varint_pack implementation.

varint_pack  medium:   643 ->  90 ns/call  (7.1x faster)
varint_pack  large:   1109 ->  96 ns/call (11.6x faster)
varint_unpack medium: 1086 -> 115 ns/call  (9.4x faster)
varint_unpack large:  1940 -> 146 ns/call (13.3x faster)
mykaul added 9 commits July 29, 2026 18:09
….from_bytes

Performance improvements to serialization/deserialization hot paths:

1. unpack_num(): Use ntohs()/ntohl() for 16-bit and 32-bit integer types
   instead of byte-by-byte swapping loop. These compile to single bswap
   instructions on x86, providing more predictable performance.

2. read_int(): Simplify to use ntohl() directly instead of going through
   unpack_num() with a temporary Buffer.

3. varint_unpack(): Replace hex string conversion with int.from_bytes().
   This eliminates string allocations and provides 4-18x speedup for the
   function itself (larger gains for longer varints).

4. Remove slice_buffer() and replaced with direct assignment

5. _unpack_len() is now implemented similar to read_int()

Also removes unused 'start' and 'end' variables from unpack_num().

End-to-end benchmark shows ~4-5% improvement in row throughput.

Signed-off-by: Yaniv Kaul <yaniv.kaul@scylladb.com>
Use hardware byte-swap intrinsic for float unmarshaling instead of manual
4-iteration loop, providing 4-8x speedup on little-endian systems.

All tests passing (609 total) [see next commit for a fix for existing Cython related issue!]

Signed-off-by: Yaniv Kaul <yaniv.kaul@scylladb.com>
Refactor deserializers.pyx to use from_ptr_and_size() consistently
instead of manual Buffer field assignment for better code clarity and
maintainability.

Changes:
- cassandra/deserializers.pyx: Refactor 4 locations to use helper

Tests: All Cython tests compile and pass (5 tests)

Signed-off-by: Yaniv Kaul <yaniv.kaul@scylladb.com>
Add comprehensive benchmark comparing different deserialization strategies
for VectorType with various numeric types and vector sizes.

The benchmark measures:
- Current element-by-element baseline
- struct.unpack bulk deserialization
- numpy frombuffer with tolist()
- numpy frombuffer zero-copy approach

Tested with common ML/AI embedding dimensions:
- Small vectors: 3-4 elements
- Medium vectors: 128-384 elements
- Large vectors: 768-1536 elements

Usage:
  export CASS_DRIVER_NO_CYTHON=1  # Test pure Python implementation
  python benchmarks/vector_deserialize.py

Includes CPU pinning for consistent measurements and result verification
to ensure correctness of all optimization approaches.

Baseline Performance (per-operation deserialization time):
  Vector<float, 3>     :  0.88 μs
  Vector<float, 4>     :  0.78 μs
  Vector<float, 128>   :  4.72 μs
  Vector<float, 384>   : 15.38 μs
  Vector<float, 768>   : 32.43 μs
  Vector<float, 1536>  : 63.74 μs
  Vector<double, 128>  :  4.83 μs
  Vector<int, 128>     :  2.27 μs

Signed-off-by: Yaniv Kaul <yaniv.kaul@scylladb.com>
…ct.unpack

Add bulk deserialization using struct.unpack for common numeric vector types
instead of element-by-element deserialization. This provides significant
performance improvements, especially for small vectors and integer types.

Optimized types:
- FloatType  ('>Nf' format)
- DoubleType ('>Nd' format)
- Int32Type  ('>Ni' format)
- LongType   ('>Nq' format)
- ShortType  ('>Nh' format)

Performance improvements (measured with CASS_DRIVER_NO_CYTHON=1):

Small vectors (3-4 elements):
  Vector<float, 3>  : 0.88 μs → 0.25 μs  (3.58x faster)
  Vector<float, 4>  : 0.78 μs → 0.28 μs  (2.79x faster)

Medium vectors (128 elements):
  Vector<float, 128>  : 4.72 μs → 4.06 μs  (1.16x faster)
  Vector<double, 128> : 4.83 μs → 4.01 μs  (1.20x faster)
  Vector<int, 128>    : 2.27 μs → 1.25 μs  (1.82x faster)

Large vectors (384-1536 elements):
  Vector<float, 384>  : 15.38 μs → 14.67 μs  (1.05x faster)
  Vector<float, 768>  : 32.43 μs → 30.72 μs  (1.06x faster)
  Vector<float, 1536> : 63.74 μs → 63.24 μs  (1.01x faster)

The optimization is most effective for:
- Small vectors (3-4 elements): 2.8-3.6x speedup
- Integer vectors: 1.8x speedup
- Medium-sized float/double vectors: 1.2-1.3x speedup

For very large vectors (384+ elements), the benefit is minimal as the
deserialization time is dominated by data copying rather than function
call overhead.

Variable-size subtypes and other numeric types continue to use the
element-by-element fallback path.

Signed-off-by: Yaniv Kaul <yaniv.kaul@scylladb.com>
For vectors with 32 or more elements, use numpy.frombuffer() which provides
1.3-1.5x speedup for large vectors (128+ elements) compared to struct.unpack.

The hybrid approach:
- Small vectors (< 32 elements): struct.unpack (2.8-3.6x faster than baseline)
- Large vectors (>= 32 elements): numpy.frombuffer().tolist() (1.3-1.5x faster than struct.unpack)

Threshold of 32 elements balances code complexity with performance gains.

Benchmark results:
- float[128]:  2.15 μs → 1.87 μs (1.15x faster)
- float[384]:  6.17 μs → 4.44 μs (1.39x faster)
- float[768]: 12.25 μs → 8.45 μs (1.45x faster)
- float[1536]: 24.44 μs → 15.77 μs (1.55x faster)

Signed-off-by: Yaniv Kaul <yaniv.kaul@scylladb.com>
…izer

Addded DesVectorType Cython deserializer with C-level optimizations for
improved performance in row parsing for vectors.
The deserializer uses:
- Direct C byte swapping (ntohl, ntohs) for numeric types
- Memory operations without Python object overhead
- Unified numpy path for large vectors (≥32 elements)
- struct.unpack fallback for small vectors (<32 elements)

Performance improvements:
- Small vectors (3-4 elements): 4.4-4.7x faster
- Medium vectors (128 elements): 1.0-1.5x faster
- Large vectors (384-1536 elements): 0.9-1.0x (marginal)

The Cython deserializer is automatically used by the row parser when
available via find_deserializer().

Includes unit tests and benchmark code.

Follow-up commits will try to get Numpy arrays, and perhaps more.

Signed-off-by: Yaniv Kaul <yaniv.kaul@scylladb.com>
Extend NumpyParser to handle VectorType columns by creating 2D NumPy
arrays (rows × vector_dimension) instead of object arrays. This enables
zero-copy parsing for vector embeddings in ML/AI workloads.

Features:
- Detects VectorType via vector_size and subtype attributes
- Creates 2D masked arrays for numeric vector subtypes (float, double,
  int32, int64, int16)
- Falls back to object arrays for unsupported vector subtypes
- Handles endianness conversion for both 1D and 2D arrays
- Pre-allocates result arrays for efficiency

Supported vector types:
- Vector<float> → 2D float32 array
- Vector<double> → 2D float64 array
- Vector<int> → 2D int32 array
- Vector<bigint> → 2D int64 array
- Vector<smallint> → 2D int16 array

Adds comprehensive test coverage for all supported vector types,
mixed column queries, and large vector dimensions (384-element embeddings).

Signed-off-by: Yaniv Kaul <yaniv.kaul@scylladb.com>
Replace POSIX-specific arpa/inet.h with conditional compilation that uses
winsock2.h on Windows and arpa/inet.h on POSIX systems.

This ensures the driver can be compiled on Windows without modification.

Changes:
- cassandra/cython_marshal.pyx: Add platform detection for ntohs/ntohl
- cassandra/ioutils.pyx: Add platform detection for ntohl

Signed-off-by: Yaniv Kaul <yaniv.kaul@scylladb.com>
…fallback

ShortType (smallint) vector elements are vint-length-prefixed on the wire
in real Cassandra (AbstractType.valueLengthIfFixed() / ShortType.java do
not override the variable-length default), not fixed 2-byte values. It
was incorrectly listed in VectorType._struct_format_map /
_numpy_dtype_map as a fixed-width fast-path candidate:

- In cqltypes.py this was dead code: VectorType.deserialize/serialize
  gate on subtype.serial_size() (which correctly returns None for
  ShortType) before ever consulting _vector_struct, so the fast path was
  never actually reachable for smallint vectors -- they always took the
  variable-size (vint-prefixed) path regardless.

- In numpy_parser.pyx's make_array(), however, this was a live bug:
  VectorType subtype dtype resolution reused _cqltype_to_numpy (the
  dict for fixed-width *scalar column* types, where smallint genuinely
  is always 2 bytes) instead of the vector-element-specific
  _struct_format_map, so Vector<smallint, N> columns would incorrectly
  get allocated a fixed 2-byte-per-element 2D array and misparse the
  actual vint-prefixed wire data.

Given real-world usage doesn't justify adding vint-handling to the
optimized path, ShortType is removed from both maps instead. Vector<smallint>
now correctly falls back to the generic/object-array path everywhere.
Updates the affected tests accordingly (round-trip via serialize/deserialize
instead of assuming fixed-width wire format; numpy_parser test now checks
the object-array fallback allocation rather than a nonexistent fixed 2D
array).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 29, 2026 16:12
@mykaul

mykaul commented Jul 29, 2026

Copy link
Copy Markdown
Author

Follow-up: removed ShortType (smallint) from the VectorType fast-path optimization.

Root cause: smallint is intentionally variable-length (vint-length-prefixed) on the wire in real Cassandra -- AbstractType.valueLengthIfFixed() and ShortType.java specifically do not override the variable-length default. It had been added to VectorType._struct_format_map/_numpy_dtype_map as if it were fixed-width:

  • In cassandra/cqltypes.py this was harmless dead code -- VectorType.deserialize/serialize already gate on subtype.serial_size() (which correctly returns None for ShortType) before ever consulting _vector_struct, so the struct-based fast path was never actually reachable for smallint vectors.
  • In cassandra/numpy_parser.pyx's make_array() it was a real bug: subtype dtype resolution reused _cqltype_to_numpy (the dict for fixed-width scalar column types, where smallint genuinely is always 2 bytes) instead of the vector-element-specific _struct_format_map, so Vector<smallint, N> columns would get allocated a fixed 2-byte-per-element 2D array and misparse the actual vint-prefixed wire data.

Since real-world usage doesn't justify adding vint-handling to the optimized path, ShortType is removed from both maps rather than patching in variable-length support. Vector<smallint, N> now correctly falls back to the generic/object-array path everywhere. Updated the affected unit tests (tests/unit/test_types.py, tests/unit/test_numpy_parser.py) to match: round-trip via serialize()/deserialize() instead of assuming a fixed-width wire format, and assert the object-array fallback allocation instead of a (no-longer-existing) fixed 2D array.

Rebuilt the Cython extensions and re-ran tests/unit/ (740 passed, 44 skipped, 0 failures) -- test_vector_cython_deserializer now passes.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (3)

cassandra/numpy_parser.pyx:156

  • Allocating fixed-width vectors as numeric arrays routes them through the raw memcpy in unpack_row, which copies the server-supplied buf.size into a row whose capacity is arr.stride without validating equality. An oversized vector can overwrite adjacent rows or heap memory, while an undersized one leaves uninitialized values. Validate buf.size == arr.stride before copying.
        if subtype in cqltypes.VectorType._struct_format_map:
            dtype = _cqltype_to_numpy[subtype]
            a = np.ma.empty((array_size, vector_size), dtype=dtype)

tests/unit/test_types.py:550

  • These calls use VectorType.deserialize rather than the des_float instance obtained above, so the test never executes the new Cython float implementation; the double, int32, and int64 sections repeat the same bypass. Invoke each DesVectorType.deserialize_bytes method so regressions in the Cython fast paths are covered.
        data_float = struct.pack('>4f', 1.0, 2.0, 3.0, 4.0)
        result_float = vt_float.deserialize(data_float, 5)

cassandra/numpy_parser.pyx:195

  • The new row-wide mask handling is not exercised by any added test: all parsed vectors are non-null, and the ShortType test only checks allocation. Add a NumPy parser case containing a null vector row and assert that the complete row is masked without masking neighboring rows.
            memset(<char *>arr.mask_ptr, 1, arr.mask_stride)

Comment thread cassandra/deserializers.pyx
Comment thread cassandra/deserializers.pyx
…serializer

find_deserializer() previously selected DesVectorType unconditionally for
any VectorType column, regardless of subtype. DesVectorType only actually
supports the fixed-width subtypes in VectorType._struct_format_map
(float/double/int32/bigint); for anything else (text, smallint, varint,
etc.) it only discovered it couldn't handle the data after construction,
via a ValueError raised deep in _deserialize_generic. Nothing in the row
parsing call chain (from_binary, TupleRowParser/ListRowParser/NumpyParser)
catches that error, so real queries against e.g. Vector<text, N> or
(post ShortType removal) Vector<smallint, N> columns would crash during
normal row parsing.

Make the dispatch subtype-aware, mirroring the pattern already used for
every other type in this function: DesVectorType only for subtypes in
_struct_format_map, GenericDeserializer (which correctly delegates to the
pure-Python VectorType.deserialize()) otherwise. No exception is raised in
the normal flow anymore, and no fallback-catching machinery is needed
elsewhere.

The ValueError inside DesVectorType._deserialize_generic is left exactly
as-is; after this fix it's unreachable through normal dispatch and now
serves as an internal invariant check (verified directly in the new
test_desvectortype_rejects_variable_size_subtype).

Updates test_vector_cython_deserializer (ShortType section) and
test_vector_cython_deserializer_variable_size_subtype, which asserted the
old unconditional-DesVectorType dispatch. Adds
test_find_deserializer_vector_dispatch to lock in the dispatch behavior
directly for both fixed-width and variable-size subtypes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 29, 2026 16:25
@mykaul

mykaul commented Jul 29, 2026

Copy link
Copy Markdown
Author

Follow-up: fixed the dispatch, not just the symptom, for the "variable-length vector subtype crashes in production" issue from the previous comment.

Root cause: find_deserializer() in cassandra/deserializers.pyx picked DesVectorType unconditionally for any VectorType column, regardless of subtype:

elif issubclass(cqltype, cqltypes.VectorType):
    cls = DesVectorType

DesVectorType only actually supports the fixed-width subtypes in VectorType._struct_format_map (float/double/int32/bigint). For anything else -- text, smallint, varint, etc. -- it only discovered it couldn't handle the data after construction, via a ValueError raised deep in _deserialize_generic. Nothing in the row-parsing call chain (from_binary, TupleRowParser/ListRowParser/NumpyParser) catches that error, so real queries against e.g. Vector<text, N> (or, after the ShortType removal, Vector<smallint, N>) columns would crash during normal row parsing.

Fix: made the dispatch subtype-aware, mirroring the pattern already used for every other type in this function:

elif issubclass(cqltype, cqltypes.VectorType):
    cls = DesVectorType if cqltype.subtype in cqltypes.VectorType._struct_format_map else GenericDeserializer

GenericDeserializer delegates to the already-correct, already-tested pure-Python VectorType.deserialize(). No exception is raised in the normal flow anymore, and no fallback-catching machinery is needed anywhere else in the call chain. The ValueError inside DesVectorType._deserialize_generic is left exactly as-is -- it's now unreachable through normal dispatch and serves as an internal invariant check instead (verified directly in the new test_desvectortype_rejects_variable_size_subtype).

Updated test_vector_cython_deserializer (ShortType section) and test_vector_cython_deserializer_variable_size_subtype, which asserted the old unconditional-DesVectorType dispatch -- the latter now also drives the deserializer through the actual production row-parsing pipeline (ListParser/make_deserializers) to prove Vector<text, N> no longer crashes. Added test_find_deserializer_vector_dispatch to lock in the dispatch behavior directly for both fixed-width and variable-size subtypes. Grepped the repo for other DesVectorType references (benchmarks, row_parser.pyx) -- nothing else assumes unconditional dispatch.

Rebuilt the Cython extensions and re-ran the full tests/unit/ suite: 742 passed, 44 skipped, 0 failures.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (6)

cassandra/deserializers.pyx:475

  • This bounds check can itself overflow because both operands are C int values. For example, an elemlen of INT32_MAX at offset 8 can wrap the sum negative, pass the check, and create an out-of-bounds buffer view from a malformed server response. Compare against the remaining size using subtraction instead.
        if offset[0] + elemlen <= buf.size:

cassandra/numpy_parser.pyx:156

  • This fixed 2D row has only arr.stride bytes available, but unpack_row copies the server-provided buf.size without checking it against that stride. An undersized vector returns uninitialized elements, while an oversized length writes past the row allocation. Validate that non-null numeric values have exactly the expected stride before memcpy.
            a = np.ma.empty((array_size, vector_size), dtype=dtype)

cassandra/deserializers.pyx:582

  • This accepts every negative length as None, including values below -2 that the new length protocol handling above explicitly classifies as invalid. A malformed tuple/UDT field with length -3 is therefore silently converted to null instead of rejected.
                elif itemlen < 0:
                    # NULL value, item stays None
                    pass
                else:
                    raise IndexError("Tuple item length %d at offset %d exceeds buffer size %d" % (itemlen, p, buf.size))

cassandra/marshal.py:43

  • int.from_bytes(b'', signed=True) returns 0, whereas the previous implementation rejected an empty varint. Zero is canonically encoded as b'\x00' by varint_pack, so this now makes malformed varints—and decimals containing only the four-byte scale—silently deserialize as zero. Preserve the empty-input validation before using from_bytes.
    return int.from_bytes(term, byteorder='big', signed=True)

cassandra/cython_marshal.pyx:88

  • The Cython path has the same empty-input regression as the Python implementation: int.from_bytes(b'', signed=True) returns zero, so a decimal payload containing only its scale is accepted even though zero varints are encoded as b'\x00'. Retain the prior rejection of empty input.
    return int.from_bytes(term, byteorder='big', signed=True)

tests/unit/test_types.py:531

  • Despite this stated purpose, the test only checks the selected class and then calls vt_*.deserialize, which exercises the pure-Python VectorType implementation. None of the float/double/int32/int64 Cython routines (or their wrong-size checks) run here. Invoke each des_* .deserialize_bytes(...) instance instead so the new Cython implementation is actually covered.
        Test that VectorType uses the Cython DesVectorType deserializer
        and correctly deserializes vectors of supported numeric types.

Comment thread cassandra/deserializers.pyx
…mismatch, numpy_parser bounds check

- setup.py: link ws2_32 on Windows for the cassandra/*.pyx extension glob.
  cython_marshal.pyx declares ntohs/ntohl via winsock2.h on Windows, and
  it is textually included into deserializers.pyx (directly) and into
  ioutils.pyx (in turn included by numpy_parser.pyx, obj_parser.pyx and
  row_parser.pyx), so every one of those extensions needs the link
  library, not just a single module.

- cassandra/deserializers.pyx: DesCompositeType.deserialize stored the
  wire's unsigned 16-bit element length in a signed int16_t, so any
  element length above 32767 wrapped negative, corrupting the bounds
  check and the subsequent buffer-advancing pointer arithmetic. Changed
  to uint16_t to match the actual wire type (also fixes
  DesDynamicCompositeType, which is an alias of DesCompositeType).

- cassandra/numpy_parser.pyx: unpack_row's memcpy trusted the
  wire-provided buf.size against the destination array's fixed stride
  with no validation, allowing an oversized server response to overflow
  into adjacent NumPy array memory. Added a buf.size != arr.stride check
  that raises ValueError before the memcpy. Also traced make_arrays()/
  make_array() to confirm the mask_ptr == 0 case only ever coincides
  with is_object arrays (handled by an earlier branch), so the memset
  branch is not reachable with a NULL mask pointer; added a comment
  documenting that invariant.

Added regression tests: two NumPy bounds-check tests (oversized/
undersized fixed-width values) in tests/unit/test_numpy_parser.py, and
a Cython-level composite-length test in
tests/unit/cython/types_testhelper.pyx exercising a >32767-byte
composite element end-to-end through DesCompositeType.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 29, 2026 16:45
@mykaul

mykaul commented Jul 29, 2026

Copy link
Copy Markdown
Author

Fixed 3 confirmed CodeRabbit findings from this PR's review (commit f2bdc13):

  1. cassandra/cython_marshal.pyx — missing Windows link library. ntohs/ntohl are declared via winsock2.h on Windows but implemented in ws2_32.lib. Since cython_marshal.pyx is textually included into both deserializers.pyx (directly) and ioutils.pyx (which is itself included by numpy_parser.pyx, obj_parser.pyx, and row_parser.pyx), every one of those compiled extensions needed the link library, not just a single module. Added libraries=['ws2_32'] if is_windows else [] to the cassandra/*.pyx extension glob in setup.py.

  2. cassandra/deserializers.pyx — signed/unsigned mismatch in DesCompositeType.deserialize. element_length was declared int16_t but assigned from an unsigned 16-bit wire read (unpack_num[uint16_t]). Any element length above 32767 wrapped negative, corrupting both the bounds check and the subsequent pointer arithmetic (out-of-bounds read). Changed to uint16_t to match the actual wire type. DesDynamicCompositeType is an alias, so this fixes both.

  3. cassandra/numpy_parser.pyx — missing bounds check before memcpy in unpack_row. The destination array slot is exactly arr.stride bytes, but the wire-provided buf.size was never validated against it before the memcpy, so an oversized (malformed/malicious) server response could overflow into adjacent NumPy array memory. Added a buf.size != arr.stride check that raises ValueError before the copy. Also traced make_arrays()/make_array() to confirm the mask_ptr == 0 case only ever coincides with is_object arrays (handled by an earlier branch), so the memset branch is not reachable with a NULL mask pointer — added a comment documenting that invariant instead of a redundant runtime guard.

Tests: Added two regression tests in tests/unit/test_numpy_parser.py (oversized/undersized fixed-width values against fix 3) and a Cython-level test in tests/unit/cython/types_testhelper.pyx exercising a >32767-byte composite element end-to-end through DesCompositeType (fix 2). Full tests/unit/ suite passes: 744 passed, 45 skipped (up from the prior 742/44 baseline — the 2 new increments are the new numpy_parser tests; the new cython test is skipped in this sandbox due to a pre-existing, unrelated HAVE_CYTHON circular-import quirk between cython_deps.py and cqltypes.py that also already skips the two existing cython type tests here — verified the fix directly bypassing that quirk, see commit description).

Resolved the 3 corresponding CodeRabbit review threads.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (10)

cassandra/deserializers.pyx:36

  • cython_deps determines HAVE_CYTHON by importing row_parser, whose dependency chain imports this module. Re-importing HAVE_NUMPY from the still-initializing cython_deps module raises ImportError, causing the probe to report that Cython is unavailable and disabling all optimized protocol handlers. This dependency must not point back to cython_deps; probe NumPy locally (or move the NumPy probe to a dependency-free module).
# Import numpy availability flag and conditionally import numpy
from cassandra.cython_deps import HAVE_NUMPY
if HAVE_NUMPY:
    import numpy as np

cassandra/deserializers.pyx:475

  • This bounds check can itself overflow because offset[0] and elemlen are C ints. For example, an element length of INT32_MAX at offset 4 can wrap the sum negative, pass the check against a small buffer, and create an out-of-bounds Buffer view. Compare against the remaining size instead so malformed length fields cannot bypass the validation.
        if offset[0] + elemlen <= buf.size:

tests/unit/test_types.py:560

  • This assertion exercises the pure-Python VectorType.deserialize, not the newly selected Cython deserializer. Use des_double.deserialize_bytes so the double byte-swapping path is actually tested.
        result_double = vt_double.deserialize(data_double, 5)

tests/unit/test_types.py:569

  • Calling the type class directly bypasses des_int32, leaving the new Cython int32 path untested despite the test's stated purpose. Drive the DesVectorType wrapper here.
        result_int32 = vt_int32.deserialize(data_int32, 5)

tests/unit/test_types.py:578

  • This uses the pure-Python deserializer and therefore never checks _deserialize_int64. Invoke the Cython deserializer instance selected above to cover its 64-bit byte swapping.
        result_int64 = vt_int64.deserialize(data_int64, 5)

tests/unit/test_types.py:602

  • The malformed-size check also bypasses DesVectorType, so it cannot catch a regression in the Cython expected_size validation added by this PR. Call des_float.deserialize_bytes here as well.
            vt_float.deserialize(struct.pack('>3f', 1.0, 2.0, 3.0), 5)  # 3 floats instead of 4

cassandra/numpy_parser.pyx:208

  • The new row-wide mask write is not exercised by the added NumPy parser tests: every fixed-width vector row contains a value. Add a case with a -1 vector length between non-null rows and assert that all elements of only that vector row are masked; this verifies both mask_stride and pointer advancement and guards against masking adjacent rows.
            memset(<char *>arr.mask_ptr, 1, arr.mask_stride)

cassandra/deserializers.pyx:483

  • Creating a negative-length Buffer works for from_binary, but not for every caller of subelem. In _deserialize_map, a NULL key is subsequently passed to to_bytes(&key_buf) at line 540; to_bytes slices buf.ptr[:buf.size], so a NULL pointer with size -1 raises instead of producing the None serialized key that the pure-Python map deserializer supports (cqltypes.py:883-900). Handle negative key buffers before calling to_bytes; otherwise maps containing NULL keys still fail on the Cython path.
    elif elemlen == -1 or elemlen == -2:
        from_ptr_and_size(NULL, elemlen, elem_buf)
        return 0

tests/unit/test_types.py:550

  • This calls VectorType.deserialize, so it bypasses the DesVectorType instance selected above and does not test the new Cython float implementation. Invoke the deserializer's Python wrapper so failures in _deserialize_float/the NumPy branch are covered.

This issue also appears in the following locations of the same file:

  • line 560
  • line 569
  • line 578
  • line 602
        result_float = vt_float.deserialize(data_float, 5)

setup.py:336

  • This link setting only applies to extensions matched by cassandra/*.pyx. The Windows Cython tests compile tests/unit/cython/types_testhelper.pyx separately via pyximport; that helper includes ioutils.pyx, calls get_buf/read_int, and now references ntohl without linking ws2_32, so its import will fail with an unresolved symbol on Windows. Ensure the pyximport build receives the same library (or implement the byte swaps without Winsock linkage).
                # cython_marshal.pyx (included by deserializers.pyx, and by ioutils.pyx which
                # is in turn included by numpy_parser.pyx/obj_parser.pyx/row_parser.pyx) declares
                # ntohs/ntohl via winsock2.h on Windows. Those symbols are implemented in
                # ws2_32.lib, so every *.pyx extension needs to link against it on Windows.
                platform_libraries = ['ws2_32'] if is_windows else []

cassandra.cython_deps determines HAVE_CYTHON by importing
cassandra.row_parser. row_parser pulls in deserializers.pyx (which
imports cqltypes), and both cassandra/deserializers.pyx and
cassandra/cqltypes.py used to import HAVE_NUMPY back out of
cassandra.cython_deps. When cython_deps was the first cassandra module
touched in a process, Python registers it in sys.modules before running
its body, so that re-entrant "from cassandra.cython_deps import
HAVE_NUMPY" hit the partially initialized module and raised ImportError
-- which cython_deps' own except clause swallowed, silently and
permanently marking Cython unavailable even though the compiled
extensions were fine.

Confirmed empirically: a fresh process running
`python3 -c "from cassandra.cython_deps import HAVE_CYTHON, HAVE_NUMPY; print(HAVE_CYTHON)"`
printed False before this change. Traced the actual failure with a
temporary debug print to cqltypes.py's unconditional import, which
fires before deserializers.pyx's own (deserializers imports cqltypes
before touching cython_deps itself) -- so both files needed the fix,
not just deserializers.pyx. Both now determine numpy availability
independently via their own try/except import, so nothing on
row_parser's import chain touches cython_deps anymore. Re-verified for
every import order (cython_deps, row_parser, deserializers, cqltypes,
protocol, cluster first): HAVE_CYTHON is now consistently True.

This was silently causing the "Cython is not available" wheel-build CI
failures on this PR -- not flaky external infra as originally
suspected -- since which cassandra module a test happens to import
first is what determined whether the bug manifested.

Also verified a separately reported concern about
DesTupleType.deserialize's bounds check (`p + itemlen <= buf.size`) as
a false positive: `p` is Py_ssize_t (64-bit) and `itemlen` is int32_t,
so C's usual arithmetic conversions promote itemlen to 64-bit before
the addition, and the comparison cannot wrap even when
itemlen == INT32_MAX.

Added tests:
- tests/unit/cython/test_cython_deps.py: subprocess-based regression
  test spawning a fresh interpreter for several import orders, asserting
  HAVE_CYTHON is reported consistently. Confirmed it fails (not skips)
  if the bug is reintroduced.
- tests/unit/cython/types_testhelper.pyx:
  test_tuple_itemlen_int32_max_no_overflow, confirming the tuple bounds
  check correctly rejects an INT32_MAX declared item length against an
  undersized buffer instead of silently overflowing. Verified the test
  is meaningful by temporarily forcing 32-bit arithmetic in the check
  and confirming the test then fails.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 29, 2026 17:35
@mykaul

mykaul commented Jul 29, 2026

Copy link
Copy Markdown
Author

Circular import fix: this was causing today's "Cython is not available" wheel-build CI failures

Pushed a fix for a real, confirmed circular import bug: cassandra.cython_deps.HAVE_CYTHON could silently and incorrectly report False depending on which cassandra module a process happened to import first -- even when the compiled Cython extensions were built and working correctly.

Root cause: cassandra.cython_deps determines HAVE_CYTHON by importing cassandra.row_parser. row_parser pulls in cassandra.deserializers (via cimport/import), which in turn imports cassandra.cqltypes. Both cassandra/deserializers.pyx and cassandra/cqltypes.py used to import HAVE_NUMPY back out of cassandra.cython_deps. When cython_deps was the first cassandra-related import in a process, Python registers it in sys.modules (empty/partial) before running its body. Its body then imports row_parser -> deserializers -> cqltypes, and cqltypes's from cassandra.cython_deps import HAVE_NUMPY re-enters the still-partially-initialized cython_deps module and raises ImportError (since HAVE_NUMPY isn't assigned yet). That ImportError propagates all the way back up and gets caught by cython_deps's own except ImportError: HAVE_CYTHON = False -- silently and permanently marking Cython as unavailable for the rest of the process, even though the compiled .so files are perfectly fine.

Confirmed empirically:

$ python3 -c "from cassandra.cython_deps import HAVE_CYTHON, HAVE_NUMPY; print(HAVE_CYTHON)"
False

...on a build with working compiled extensions, when cython_deps was the first cassandra import. Traced the exact failure with a temporary debug print in the except clause -- it actually fires in cassandra/cqltypes.py's import (line 53), which runs before deserializers.pyx ever reaches its own cython_deps import (line 34), since deserializers.pyx imports cqltypes first. So both files needed fixing, not just the one flagged in review.

This explains why the failures showed up inconsistently across CI runs: whichever module a given test file (or its collection order) happened to import first determined whether the bug manifested -- not flaky external infra as originally suspected.

Fix: both cassandra/deserializers.pyx and cassandra/cqltypes.py now determine HAVE_NUMPY independently via their own local try: import numpy as np / except ImportError, instead of importing the flag from cassandra.cython_deps. Nothing on row_parser's import chain touches cython_deps anymore. Re-verified after the fix for every import order (cython_deps first, row_parser first, deserializers first, cqltypes first, protocol first, cluster first) -- HAVE_CYTHON is now consistently True.

Added tests/unit/cython/test_cython_deps.py, a subprocess-based regression test that spawns a fresh interpreter for each import order and asserts HAVE_CYTHON. Confirmed it fails (not skips) if the bug is reintroduced.

I also verified the reported DesTupleType.deserialize bounds-check overflow concern and confirmed it's a false positive (see inline reply), and verified + resolved four other review threads that later commits had already addressed -- see inline replies on each for what was checked.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (3)

cassandra/cqltypes.py:1470

  • Using issubclass here changes the behavior of registered custom numeric subtypes. CassandraTypeType registers subclasses and lookup_casstype accepts them, but the cached struct path bypasses their overridden serialize/deserialize methods in both directions. Restrict this optimization to the exact built-in subtype, as the NumPy and Cython dispatch paths already do, so custom subclasses retain their conversion semantics.
        for base_type, fmt_char in cls._struct_format_map.items():
            if subtype is base_type or (isinstance(subtype, type) and issubclass(subtype, base_type)):
                vector_struct = struct.Struct(f'>{vsize}{fmt_char}')
                numpy_dtype = cls._numpy_dtype_map.get(fmt_char)
                break

setup.py:348

  • This links the production cassandra/*.pyx extensions, but not the test extension built by pyximport in tests/unit/cython/utils.py:29-32. types_testhelper.pyx includes ioutils.pyx, which now includes the new ntohl/ntohs calls, so on Windows that helper will fail to link with unresolved Winsock symbols. Please also supply ws2_32 when building the pyximport test helper (or use a byte-swap implementation that has no external linker dependency).
                    NoPatchExtension("*", ["cassandra/*.pyx"], extra_compile_args=compile_args,
                                      libraries=platform_libraries),

tests/unit/test_types.py:531

  • This test never exercises DesVectorType: every successful and error-path assertion calls vt_*.deserialize, which is the pure-Python classmethod, after only checking the Cython deserializer's class name. Consequently the new Cython float/double/int32/int64 implementations can regress while this test still passes. Drive these cases through each des_*.deserialize_bytes(...) instance instead.
    def test_vector_cython_deserializer(self):
        """
        Test that VectorType uses the Cython DesVectorType deserializer
        and correctly deserializes vectors of supported numeric types.

mykaul added a commit to mykaul/python-driver that referenced this pull request Jul 29, 2026
…bytes

Replace the manual string-formatting hex conversion in varint_unpack()
and the byte-by-byte bytearray loop in varint_pack() with Python 3
builtins int.from_bytes() and int.to_bytes().

varint_unpack used '%02x' formatting per byte, str.join, then
int(..., 16) to parse back — O(n) string allocations.  int.from_bytes
is a single C-level call.

varint_pack used a while loop appending individual bytes to a bytearray,
then reversing.  int.to_bytes computes the result in one C call.

Also fixes the Cython path in cython_marshal.pyx which had the same
slow pattern with a TODO comment to optimize.

Adapted from PR scylladb#689 (varint_unpack) with new varint_pack implementation.

varint_pack  medium:   643 ->  90 ns/call  (7.1x faster)
varint_pack  large:   1109 ->  96 ns/call (11.6x faster)
varint_unpack medium: 1086 -> 115 ns/call  (9.4x faster)
varint_unpack large:  1940 -> 146 ns/call (13.3x faster)
mykaul added a commit to mykaul/python-driver that referenced this pull request Jul 30, 2026
…y large vector deserialization

Add test_vector_cython_deserializer_variable_size_subtype to verify
find_deserializer()'s current dispatch for a VectorType with a
variable-size subtype (e.g. UTF8Type). cassandra.deserializers has no
dedicated Des* class for VectorType today, and VectorType is not a
subclass of any of the collection types find_deserializer()
special-cases, so it always falls through to GenericDeserializer --
regardless of whether the subtype has a fixed serialized size or not.
GenericDeserializer simply delegates to the pure Python
VectorType.deserialize(), which correctly round-trips variable-size
subtypes; the test asserts both the dispatch and the round-trip.

A Cython fast-path deserializer for VectorType (DesVectorType), with
dispatch that fast-paths fixed-size subtypes while leaving
variable-size subtypes on GenericDeserializer, is being developed in
companion PRs (scylladb#689, scylladb#732).
Neither has merged, so DesVectorType does not exist on this branch or
on master; this test intentionally does not depend on it. Once one of
those PRs lands, this test should be revisited to also assert the
Cython dispatch/behavior for fixed-size subtypes.

Add test_vector_numpy_large_deserialization to exercise VectorType
deserialization for vectors with >= 32 elements across all supported
fixed-size numeric types (float, double, int32, int64). VectorType
has no numpy-specific fast path today; the test name/threshold is
forward-looking and documents that, guarding correctness of the
existing implementation at representative embedding sizes in the
meantime.

Signed-off-by: Yaniv Kaul <yaniv.kaul@scylladb.com>
mykaul added a commit to mykaul/python-driver that referenced this pull request Jul 30, 2026
…y large vector deserialization

Add test_vector_cython_deserializer_variable_size_subtype to verify
find_deserializer()'s current dispatch for a VectorType with a
variable-size subtype (e.g. UTF8Type). cassandra.deserializers has no
dedicated Des* class for VectorType today, and VectorType is not a
subclass of any of the collection types find_deserializer()
special-cases, so it always falls through to GenericDeserializer --
regardless of whether the subtype has a fixed serialized size or not.
GenericDeserializer simply delegates to the pure Python
VectorType.deserialize(), which correctly round-trips variable-size
subtypes; the test asserts both the dispatch and the round-trip.

A Cython fast-path deserializer for VectorType (DesVectorType), with
dispatch that fast-paths fixed-size subtypes while leaving
variable-size subtypes on GenericDeserializer, is being developed in
companion PRs (scylladb#689, scylladb#732).
Neither has merged, so DesVectorType does not exist on this branch or
on master; this test intentionally does not depend on it. Once one of
those PRs lands, this test should be revisited to also assert the
Cython dispatch/behavior for fixed-size subtypes.

Add test_vector_numpy_large_deserialization to exercise VectorType
deserialization for vectors with >= 32 elements across all supported
fixed-size numeric types (float, double, int32, int64). VectorType
has no numpy-specific fast path today; the test name/threshold is
forward-looking and documents that, guarding correctness of the
existing implementation at representative embedding sizes in the
meantime.

Signed-off-by: Yaniv Kaul <yaniv.kaul@scylladb.com>
mykaul added a commit to mykaul/python-driver that referenced this pull request Jul 30, 2026
…y large vector deserialization

Add test_vector_cython_deserializer_variable_size_subtype to verify
find_deserializer()'s current dispatch for a VectorType with a
variable-size subtype (e.g. UTF8Type). cassandra.deserializers has no
dedicated Des* class for VectorType today, and VectorType is not a
subclass of any of the collection types find_deserializer()
special-cases, so it always falls through to GenericDeserializer --
regardless of whether the subtype has a fixed serialized size or not.
GenericDeserializer simply delegates to the pure Python
VectorType.deserialize(), which correctly round-trips variable-size
subtypes; the test asserts both the dispatch and the round-trip.

A Cython fast-path deserializer for VectorType (DesVectorType), with
dispatch that fast-paths fixed-size subtypes while leaving
variable-size subtypes on GenericDeserializer, is being developed in
companion PRs (scylladb#689, scylladb#732).
Neither has merged, so DesVectorType does not exist on this branch or
on master; this test intentionally does not depend on it. Once one of
those PRs lands, this test should be revisited to also assert the
Cython dispatch/behavior for fixed-size subtypes.

Add test_vector_numpy_large_deserialization to exercise VectorType
deserialization for vectors with >= 32 elements across all supported
fixed-size numeric types (float, double, int32, int64). VectorType
has no numpy-specific fast path today; the test name/threshold is
forward-looking and documents that, guarding correctness of the
existing implementation at representative embedding sizes in the
meantime.

Signed-off-by: Yaniv Kaul <yaniv.kaul@scylladb.com>
mykaul added a commit to mykaul/python-driver that referenced this pull request Jul 31, 2026
…bytes

Replace the manual string-formatting hex conversion in varint_unpack()
and the byte-by-byte bytearray loop in varint_pack() with Python 3
builtins int.from_bytes() and int.to_bytes().

varint_unpack used '%02x' formatting per byte, str.join, then
int(..., 16) to parse back — O(n) string allocations.  int.from_bytes
is a single C-level call.

varint_pack used a while loop appending individual bytes to a bytearray,
then reversing.  int.to_bytes computes the result in one C call.

Also fixes the Cython path in cython_marshal.pyx which had the same
slow pattern with a TODO comment to optimize.

Adapted from PR scylladb#689 (varint_unpack) with new varint_pack implementation.

varint_pack  medium:   643 ->  90 ns/call  (7.1x faster)
varint_pack  large:   1109 ->  96 ns/call (11.6x faster)
varint_unpack medium: 1086 -> 115 ns/call  (9.4x faster)
varint_unpack large:  1940 -> 146 ns/call (13.3x faster)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants