Skip to content

perf: optimize vector marshal/unmarshal for float32/float64/int32/int64 (Throughput: 75 MiB/s → 1.9 GiB/s (marshal), 106 MiB/s → 4.6 GiB/s (unmarshal) ) - #770

Draft
mykaul wants to merge 7 commits into
scylladb:masterfrom
mykaul:vector-perf-optimize

Conversation

@mykaul

@mykaul mykaul commented Mar 13, 2026

Copy link
Copy Markdown

Summary

Type-specialized fast paths for vector<float>, vector<double>, vector<int>, vector<bigint>, and vector<uuid>/vector<timeuuid> that bypass reflect-based per-element marshaling in favor of direct encoding/binary bulk conversion, plus sync.Pool buffer reuse wired into the connection write path, and a VectorType.NewWithError() fast path that eliminates the expensive goType()asVectorType() re-parse on every call.

Commit 1: d527db1 perf: optimize vector marshal/unmarshal for float32/float64/int32/int64

Fast-path type switches, 8 dedicated marshal/unmarshal functions, sync.Pool infrastructure (getVectorBuf/putVectorBuf), unmarshal slice reuse, generic-path buf.Grow() preallocation via vectorFixedElemSize(), and comprehensive tests (58 subtests across 13 categories).

Commit 2: 04f0783 perf: wire putVectorBuf into connection write path

Adds defer putVectorBuf(...) calls in executeQuery() and executeBatch() in conn.go, so production callers return pooled marshal buffers after the framer copies them. This closes the pool lifecycle and achieves 48 B/op steady-state on the write path.

Commit 3: d48f44f perf: add UUID/TimeUUID vector fast path

Adds marshal/unmarshal fast paths for vector<uuid> and vector<timeuuid> — bulk copy() of fixed 16-byte elements with zero per-element allocations. UUID vectors are common in similarity search use cases (storing document IDs alongside embeddings).

Commit 4: 66ed9aa perf: add VectorType.NewWithError() to avoid goType/asVectorType re-parse

VectorType embeds NativeType but had no NewWithError() method. When called, it dispatched to NativeType.NewWithError() which hit TypeCustomgoType()asVectorType(), re-parsing the full Java type string on every call. The new method returns *[]SubType directly: 10.7x faster (181ns → 17ns), 75% fewer allocs (4 → 1), 74% less memory (92B → 24B).

Headline numbers (vector<float, 1536>, typical embedding dimension)

  • 22x faster marshal (fast paths alone), 41x with pool recycling (see Pooled benchmarks)
  • 36x faster unmarshal, zero allocations steady state
  • 99.93% fewer allocations on marshal (3,074 → 2)
  • Marshal memory: 18,456 B/op → 6,172 B/op (fast paths) → 48 B/op (with pool recycling)
  • Unmarshal memory: 6,168 B/op → 0 B/op
  • 10.7x faster NewWithError() for VectorType (181ns → 17ns)

Benchmark results

All benchmarks: 6 iterations, benchstat, all p=0.002. Machine: 12th Gen Intel Core i7-1270P.

Master = 3881f1e (origin/master), Optimized = 66ed9aa (this branch HEAD).

Latency (ns/op) — Master vs Optimized (all 4 commits)

Benchmark Master Optimized Speedup
float32
MarshalVectorFloat32/dim_128 4,641 285 16.3x
MarshalVectorFloat32/dim_384 16,237 699 23.2x
MarshalVectorFloat32/dim_768 26,785 1,234 21.7x
MarshalVectorFloat32/dim_1536 54,049 2,415 22.4x
UnmarshalVectorFloat32/dim_128 3,116 95 32.8x
UnmarshalVectorFloat32/dim_384 10,647 280 38.0x
UnmarshalVectorFloat32/dim_768 18,536 586 31.6x
UnmarshalVectorFloat32/dim_1536 39,186 1,092 35.9x
float64
MarshalVectorFloat64/dim_128 4,700 380 12.4x
MarshalVectorFloat64/dim_384 13,125 985 13.3x
MarshalVectorFloat64/dim_768 29,809 1,905 15.6x
MarshalVectorFloat64/dim_1536 59,207 3,754 15.8x
int32
MarshalVectorInt32/dim_128 4,599 249 18.5x
MarshalVectorInt32/dim_384 13,560 654 20.7x
MarshalVectorInt32/dim_768 25,402 1,166 21.8x
MarshalVectorInt32/dim_1536 47,432 2,262 21.0x
UnmarshalVectorInt32/dim_128 3,201 94 34.1x
UnmarshalVectorInt32/dim_384 9,763 279 35.0x
UnmarshalVectorInt32/dim_768 19,700 547 36.0x
UnmarshalVectorInt32/dim_1536 40,106 1,073 37.4x
int64
MarshalVectorInt64/dim_128 4,643 368 12.6x
MarshalVectorInt64/dim_384 13,578 952 14.3x
MarshalVectorInt64/dim_768 26,887 1,834 14.7x
MarshalVectorInt64/dim_1536 62,726 3,636 17.3x
UnmarshalVectorInt64/dim_128 3,387 111 30.5x
UnmarshalVectorInt64/dim_384 9,854 343 28.7x
UnmarshalVectorInt64/dim_768 21,628 601 36.0x
UnmarshalVectorInt64/dim_1536 44,880 1,190 37.7x
UUID
MarshalVectorUUID/dim_128 8,154 710 11.5x
MarshalVectorUUID/dim_384 25,949 1,994 13.0x
MarshalVectorUUID/dim_768 52,738 3,861 13.7x
MarshalVectorUUID/dim_1536 95,785 7,480 12.8x
UnmarshalVectorUUID/dim_128 3,688 119 31.0x
UnmarshalVectorUUID/dim_384 11,164 330 33.8x
UnmarshalVectorUUID/dim_768 22,201 651 34.1x
UnmarshalVectorUUID/dim_1536 44,435 1,294 34.3x
NewWithError / RowData
VectorNewWithError/VectorType 181 17 10.7x
VectorNewWithError/NativeType_fallback 183 168 1.1x
RowDataWithVector 503 108 4.7x

Pool wiring benefit (Commit 2) — production write path

The table above measures Marshal()/Unmarshal() via the public API, which does not return buffers to the pool. In production, executeQuery()/executeBatch() return the buffer via putVectorBuf() after the framer copies it. The Pooled benchmarks simulate this:

Benchmark Master Fast-path only Speedup vs master + Pool return Speedup vs master
MarshalFloat32Pooled/dim_128 4,641 285 16.3x 161 28.8x
MarshalFloat32Pooled/dim_384 16,237 699 23.2x 369 44.0x
MarshalFloat32Pooled/dim_768 26,785 1,234 21.7x 679 39.4x
MarshalFloat32Pooled/dim_1536 54,049 2,415 22.4x 1,306 41.4x
MarshalInt32Pooled/dim_1536 47,432 2,262 21.0x 1,100 43.1x
MarshalInt64Pooled/dim_1536 62,726 3,636 17.3x 1,299 48.3x
MarshalUUIDPooled/dim_1536 95,785 7,480 12.8x 1,440 66.5x

Memory with pool return is 48 B/op constant, regardless of vector dimension or element type (from sync.Pool interface boxing overhead, irreducible). Compare to master: 18,456 B/op for float32/dim_1536, 98,328 B/op for UUID/dim_1536.

Full benchstat details: memory and allocations (click to expand)

Memory (B/op)

Benchmark Master Optimized Change
RowDataWithVector 216 144 -33.33%
UnmarshalVectorFloat32/dim_128 536 0 -100.00%
UnmarshalVectorFloat32/dim_384 1,560 0 -100.00%
UnmarshalVectorFloat32/dim_768 3,096 0 -100.00%
UnmarshalVectorFloat32/dim_1536 6,168 0 -100.00%
MarshalVectorFloat32/dim_128 1,560 536 -65.64%
MarshalVectorFloat32/dim_384 4,632 1,561 -66.30%
MarshalVectorFloat32/dim_768 9,240 3,098 -66.47%
MarshalVectorFloat32/dim_1536 18,456 6,172 -66.55%
MarshalVectorFloat64/dim_128 3,096 1,048 -66.15%
MarshalVectorFloat64/dim_384 9,240 3,098 -66.47%
MarshalVectorFloat64/dim_768 18,456 6,173 -66.55%
MarshalVectorFloat64/dim_1536 36,888 12,319 -66.60%
MarshalVectorInt32/dim_128 1,560 536 -65.64%
MarshalVectorInt32/dim_384 4,632 1,561 -66.30%
MarshalVectorInt32/dim_768 9,240 3,098 -66.47%
MarshalVectorInt32/dim_1536 18,456 6,172 -66.55%
MarshalVectorInt64/dim_128 3,096 1,048 -66.15%
MarshalVectorInt64/dim_384 9,240 3,098 -66.47%
MarshalVectorInt64/dim_768 18,456 6,173 -66.55%
MarshalVectorInt64/dim_1536 36,888 12,319 -66.60%
MarshalVectorUUID/dim_128 8,216 2,072 -74.77%
MarshalVectorUUID/dim_384 24,600 6,172 -74.91%
MarshalVectorUUID/dim_768 49,176 12,312 -74.94%
MarshalVectorUUID/dim_1536 98,328 24,600 -74.96%
UnmarshalVectorInt32 (all dims) 536–6,168 0 -100.00%
UnmarshalVectorInt64 (all dims) 1,048–12,312 0 -100.00%
UnmarshalVectorUUID (all dims) 2,072–24,600 0 -100.00%
VectorNewWithError/VectorType 92 24 -73.91%

Allocations (allocs/op)

Benchmark Master Optimized Change
RowDataWithVector 8 5 -37.50%
MarshalVectorFloat32/dim_128 258 2 -99.22%
MarshalVectorFloat32/dim_384 770 2 -99.74%
MarshalVectorFloat32/dim_768 1,538 2 -99.87%
MarshalVectorFloat32/dim_1536 3,074 2 -99.93%
MarshalVectorFloat64 (all dims) 258–3,074 2 -99.22% to -99.93%
MarshalVectorInt32 (all dims) 258–3,074 2 -99.22% to -99.93%
MarshalVectorInt64 (all dims) 258–3,074 2 -99.22% to -99.93%
MarshalVectorUUID/dim_128 386 2 -99.48%
MarshalVectorUUID/dim_384 1,154 2 -99.83%
MarshalVectorUUID/dim_768 2,306 2 -99.91%
MarshalVectorUUID/dim_1536 4,610 2 -99.96%
All unmarshal (all types, all dims) 2 0 -100.00%
VectorNewWithError/VectorType 4 1 -75.00%

Pooled marshal memory (B/op) — with pool return

Benchmark B/op
MarshalFloat32Pooled (all dims) 48
MarshalInt32Pooled (all dims) 48
MarshalInt64Pooled (all dims) 48
MarshalUUIDPooled (all dims) 48

What changed

marshal.go

  1. Fast-path type switches in marshalVector() and unmarshalVector() — before the existing reflect-based generic path, a switch on info.SubType.Type() intercepts []float32, []float64, []int32, []int64, []UUID and dispatches to 10 dedicated functions. Falls through to the generic path for all other types.

  2. 10 new marshal/unmarshal functionsmarshalVectorFloat32/Float64/Int32/Int64/UUID and corresponding unmarshalVector*. Float/int functions use encoding/binary.BigEndian.PutUint32/PutUint64 with math.Float32bits/Float64bits. UUID functions use bulk copy() of 16-byte elements.

  3. sync.Pool buffer reusevectorBufPool, getVectorBuf(size), putVectorBuf(buf) with 64 KiB cap guard.

  4. Unmarshal slice reuse — All unmarshal fast paths reuse the destination slice's backing array when capacity is sufficient, achieving zero allocations on repeated reads.

  5. Generic path preallocationvectorFixedElemSize() returns the wire-format byte size for fixed-length CQL types. The generic path calls buf.Grow() upfront.

  6. VectorType.NewWithError() — Returns *[]SubType directly without going through goType()asVectorType(). 10.7x faster, 75% fewer allocs.

conn.go

  1. Pool return wiringexecuteQuery() and executeBatch() call defer putVectorBuf(...) on each queryValues.value, closing the pool lifecycle.

Test files

  • marshal_vector_test.go — 58 unit subtests across 13 categories including UUID-specific tests
  • vector_bench_test.go — Benchmarks for all 5 types: marshal, pooled marshal, unmarshal, across dimensions 128/384/768/1536
  • marshal_test.goTestVectorNewWithErrorConsistentWithGoType, TestVectorNewWithErrorReturnsSlicePointer
  • helpers_bench_test.goBenchmarkVectorNewWithError, BenchmarkRowDataWithVector

How the bottleneck was eliminated

The original generic path for vector<float, 1536>:

  1. Called Marshal() 1,536 times through reflect dispatch
  2. Each call allocated a 4-byte []byte via encFloat32 (1,536 allocs)
  3. Appended each to a bytes.Buffer that grew incrementally (additional allocs)
  4. The buffer was then copied into framer.buf by writeBytes()

The fast path:

  1. Single getVectorBuf(6144) (pooled, zero-alloc steady state)
  2. Tight loop: binary.BigEndian.PutUint32(buf[i*4:], math.Float32bits(v))
  3. No reflect, no per-element dispatch, no intermediate allocations
  4. putVectorBuf() returns the buffer to the pool after c.exec()

Design decisions

  • Phase 3 (write-through to framer) was deliberately skipped — encoding directly into framer.buf would save ~0.4 µs (~21% marginal over the pooled path) but requires invasive changes to the queryValues struct used by all query paths. The risk/reward ratio is unfavorable.
  • Phase 4 (fix isVectorVariableLengthType) was deliberately skipped — there is a discrepancy in how some types are handled between Cassandra and ScyllaDB implementations. We focus only on the 5 types where the wire format is unambiguous.

Relationship to open PRs

Replaces PR #744 (float fast paths) and PR #745 (generic prealloc)

This PR is a strict superset of both:

Feature PR #744 PR #745 This PR
Float32/Float64 fast marshal yes yes
Float32/Float64 fast unmarshal + slice reuse yes yes
Int32/Int64 fast marshal/unmarshal yes
UUID/TimeUUID fast marshal/unmarshal yes
sync.Pool buffer reuse yes
Pool wiring in conn.go yes
VectorType.NewWithError() yes
vectorFixedElemSize() helper yes yes
Generic buf.Grow() prealloc yes yes

If this PR merges first, #744 and #745 become no-ops and should be closed.

Orthogonal to PRs #751, #752, #753

These PRs reduce per-request allocations in the connection/framing layer. This PR reduces allocations in the marshal/unmarshal layer. They are fully complementary — different files, different allocation sites, additive benefits.

Note: PR #749 (pool write-side framers) was closed — fully superseded by 3e1e7e4 on master.

Depends on PR #838

PR #838 fixes a pre-existing build failure in session_unit_test.go where hostId changed from string to UUID but test literals were not updated. This branch carries the same fix; once #838 merges, the fix becomes a no-op on rebase.

@mykaul
mykaul marked this pull request as draft March 13, 2026 11:52
@mykaul
mykaul requested a review from Copilot March 13, 2026 11: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 PR introduces high-performance, type-specialized marshal/unmarshal paths for common numeric vector element types to reduce reflect overhead and allocations in the gocql CQL codec layer.

Changes:

  • Added fast paths in marshalVector / unmarshalVector for []float32, []float64, []int32, []int64 plus a sync.Pool-backed buffer helper for marshal-side reuse.
  • Added a generic-path preallocation helper (vectorFixedElemSize + buf.Grow) to reduce bytes.Buffer growth for fixed-size element types.
  • Added extensive unit tests for vector behavior and expanded internal/public benchmarks for new int32/int64 vector cases and pooled scenarios.

Reviewed changes

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

File Description
marshal.go Adds vector fast paths, vector buffer pool helpers, and generic vector preallocation support.
marshal_vector_test.go New comprehensive unit tests for vector fast paths, pooling helpers, and preallocation behavior.
vector_bench_test.go Adds/extends internal benchmarks for pooled marshal and int32/int64 vector performance.
tests/bench/bench_vector_public_test.go Extends public API benchmarks to cover int32/int64 vector marshal/unmarshal via gocql.Marshal/Unmarshal.

Comment thread marshal.go Outdated
Comment thread marshal.go
Comment thread marshal_vector_test.go
Comment thread marshal_vector_test.go

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

Introduces type-specialized fast paths for vector marshal/unmarshal (float32/float64/int32/int64) in marshal.go to avoid reflect-heavy per-element encoding, plus expanded benchmarks and a new, comprehensive unit-test suite to validate correctness and performance characteristics.

Changes:

  • Add fast-path vector marshal/unmarshal implementations using encoding/binary bulk conversion and destination-slice reuse.
  • Add sync.Pool-backed buffer helpers (getVectorBuf/putVectorBuf) and generic-path preallocation via vectorFixedElemSize.
  • Expand internal and public benchmarks; add a large new unit test file covering many vector edge cases.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
marshal.go Adds specialized vector marshal/unmarshal fast paths, pooling helpers, and generic-path preallocation.
marshal_vector_test.go New unit tests for vector behavior (round-trip, byte-compat, slice reuse, pool behavior, etc.).
vector_bench_test.go Adds pooled/write-path/round-trip benchmarks plus int32/int64 benchmark coverage.
tests/bench/bench_vector_public_test.go Extends public API benchmarks to cover int32/int64 vectors.

Comment thread marshal.go Outdated
Comment thread marshal_vector_test.go Outdated
Comment thread marshal_vector_test.go Outdated
Comment thread marshal_vector_test.go Outdated
Comment thread marshal_vector_test.go Outdated

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 PR accelerates CQL vector encoding/decoding in the GoCQL driver by introducing type-specialized marshal/unmarshal fast paths for common numeric vector element types, reducing reflection overhead and allocations on hot paths.

Changes:

  • Add specialized marshal/unmarshal implementations for []float32, []float64, []int32, []int64 using encoding/binary + bit conversions, with unmarshal slice reuse.
  • Introduce vectorBufPool (sync.Pool) helpers for reusable marshal buffers and add generic-path preallocation via vectorFixedElemSize() + bytes.Buffer.Grow().
  • Add extensive unit tests for vector behavior and expand internal + public benchmarks for the new fast paths.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 9 comments.

File Description
marshal.go Adds vector fast paths, buffer pooling helpers, and generic-path preallocation helper.
marshal_vector_test.go New unit test suite covering round-trip, compatibility, reuse, pool behavior, and prealloc.
vector_bench_test.go Adds pooled/unpooled benchmarks and a simulated write-path benchmark for vectors.
tests/bench/bench_vector_public_test.go Extends public API benchmarks to cover int32/int64 vectors.

Comment thread marshal.go Outdated
Comment thread marshal.go Outdated
Comment thread marshal.go Outdated
Comment thread marshal.go Outdated
Comment thread marshal.go
Comment thread marshal.go Outdated
Comment thread marshal.go Outdated
Comment thread marshal.go Outdated
Comment thread marshal.go Outdated

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 PR adds specialized, non-reflect fast paths for marshaling/unmarshaling common vector<> element types in the GoCQL driver to significantly reduce allocations and improve throughput, along with extensive tests and expanded benchmarks.

Changes:

  • Add type-specialized vector marshal/unmarshal implementations for []float32, []float64, []int32, and []int64, plus a pooled []byte buffer facility for marshal fast paths.
  • Improve generic vector marshal performance via preallocation (buf.Grow) when element wire size is known.
  • Add a comprehensive unit test suite for vector behavior and extend internal + public benchmarks for the new int32/int64 paths and pooled scenarios.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.

File Description
marshal.go Adds vector fast paths, buffer pooling helpers, overflow guards, and generic-path preallocation.
marshal_vector_test.go New comprehensive unit tests covering fast paths, edge cases, and pool behavior.
vector_bench_test.go Adds pooled/write-path/round-trip benchmarks and int32/int64 benchmarks.
tests/bench/bench_vector_public_test.go Extends public API benchmarks to cover int32/int64 vectors.

Comment thread marshal.go
Comment thread marshal.go Outdated
Comment thread marshal.go
Comment thread marshal.go
Comment thread marshal.go
Comment thread marshal.go

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 PR introduces type-specialized fast paths for marshaling/unmarshaling common fixed-width vector element types (float32/float64/int32/int64) to avoid reflect-based per-element work, reducing allocations and significantly improving throughput in the driver’s vector serialization layer.

Changes:

  • Added fast-path dispatch in marshalVector/unmarshalVector with dedicated bulk encode/decode implementations for float32/float64/int32/int64 vectors.
  • Introduced a sync.Pool-backed byte buffer reuse mechanism for vector marshaling and slice-backing reuse for unmarshaling.
  • Added extensive unit tests plus expanded internal/public benchmarks for the new fast paths and pooled usage patterns.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
marshal.go Adds fast-path vector marshal/unmarshal implementations, buffer pooling helpers, and generic-path preallocation.
marshal_vector_test.go Adds a comprehensive unit test suite for vector behavior, pooling, edge cases, and compatibility.
vector_bench_test.go Adds/extends internal benchmarks for pooled marshal, write-path simulation, and int vector types.
tests/bench/bench_vector_public_test.go Extends public API benchmarks to cover int32/int64 vector marshal/unmarshal.

Comment thread marshal_vector_test.go
Comment thread marshal.go Outdated
Comment thread marshal.go
@mykaul
mykaul force-pushed the vector-perf-optimize branch from 9e5efc1 to 234f670 Compare March 17, 2026 18:04
@mykaul

mykaul commented Mar 17, 2026

Copy link
Copy Markdown
Author

Addressed review feedback:

Fixed (this push):

  1. getVectorBuf(0) now returns non-nil empty slice instead of nil. Previously, marshaling a non-nil empty vector ([]float32{}) with dim==0 would return nil, which framer.writeBytes encodes as CQL NULL. Now it correctly returns make([]byte, 0), distinguishing empty vectors from NULL.
  2. Added dim==0 array validation: When Dimensions==0 and the destination is *[N]T where N!=0, we now return an error ("array of size N cannot store vector of 0 dimensions") instead of silently succeeding and leaving the array unchanged.
  3. Strengthened empty-vector tests: All 4 TestMarshalVector_EmptyVector subtests now assert data != nil in addition to len(data) == 0, catching the nil-vs-empty distinction.

Already fixed in prior revisions:

  • Magic number 0x0015uint16(TypeDuration) (already done)
  • Displaced isVectorVariableLengthType doc comment (already adjacent to function)
  • -0 sign bit preservation tests (already have explicit Float32bits/Float64bits checks)
  • dim==0 unmarshal fast-paths already return non-nil empty slices via make([]float32, 0) etc.

Not a bug (Copilot false positives):

  • &result[:1][0] on make([]float32, 0, dim+10): This does NOT panic. Go allows reslicing up to capacity, so result[:1] is valid when cap(result) >= 1.
  • dim * 4 overflow: Already handled by vectorByteSize() which uses int64 arithmetic and checks for overflow.
  • vectorByteSize returning fmt.Errorf: All callers already wrap the error with marshalErrorf/unmarshalErrorf, so the final error type is correct.

Not changing (style preference):

  • readColWithSpec high-arity signature: Internal function with 2 call sites in the same file. A struct would add indirection without clear benefit.
  • Missing fixed-size types in vectorFixedElemSize: The function covers the types that have vector fast-paths. Other types fall through to the generic path which handles them correctly.

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

Adds type-specialized marshal/unmarshal fast paths for common vector<...> element types to significantly reduce reflection overhead and allocations in the driver’s value encoding/decoding layer.

Changes:

  • Introduces specialized marshal/unmarshal implementations for []float32, []float64, []int32, []int64 plus a sync.Pool-backed buffer helper for marshal fast paths.
  • Adds generic-path preallocation for fixed-size vector element types and improves 0-dimension handling in unmarshal.
  • Expands benchmarks and adds a comprehensive new unit test suite for vector behavior/performance characteristics.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
marshal.go Adds vector fast paths, pooled buffer helpers, fixed-element-size prealloc, and 0-dimension unmarshal handling.
marshal_vector_test.go New, extensive unit tests for vector marshal/unmarshal correctness, edge cases, and pooling behavior.
vector_bench_test.go Extends internal benchmarks to cover pooled write-path simulations and int32/int64 vectors.
tests/bench/bench_vector_public_test.go Extends public API benchmarks to include int32/int64 vector marshal/unmarshal.

Comment thread marshal_vector_test.go Outdated
Comment thread marshal.go
Comment thread marshal_vector_test.go Outdated
Comment thread marshal_vector_test.go Outdated
Comment thread marshal_vector_test.go Outdated
@mykaul
mykaul force-pushed the vector-perf-optimize branch from b0a2d82 to 118e06c Compare March 24, 2026 18:21
@mykaul
mykaul force-pushed the vector-perf-optimize branch 2 times, most recently from 432f624 to 9410b42 Compare April 4, 2026 11:54
@dkropachev

Copy link
Copy Markdown
Collaborator

@mykaul , it is great idea to use pooled buffers, but i think we need to make it generic so that it works for every data type the same way, and don't see any point in targeting vectors specifically.

@mykaul mykaul changed the title perf: optimize vector marshal/unmarshal for float32/float64/int32/int64 perf: optimize vector marshal/unmarshal for float32/float64/int32/int64 (Throughput: 75 MiB/s → 1.9 GiB/s (marshal), 106 MiB/s → 4.6 GiB/s (unmarshal) ) Apr 7, 2026
@mykaul

mykaul commented Apr 10, 2026

Copy link
Copy Markdown
Author

@mykaul , it is great idea to use pooled buffers, but i think we need to make it generic so that it works for every data type the same way, and don't see any point in targeting vectors specifically.

@dkropachev - just because they are large I targeted vectors. I can have a pool per type - or do you prefer one general pool for all types?

@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
📝 Walkthrough

Walkthrough

This PR implements vector marshaling optimizations and buffer pooling for the gocql Cassandra driver. The core change adds reflection-free fast paths in marshalVector and unmarshalVector for common element types (float32/float64/int32/int64/UUID), dispatching to type-specific big-endian encoding helpers. A sync.Pool-backed buffer allocator reduces allocations during marshal operations. Query and batch execution integrate this pooling by deferring buffer returns after the framer copies marshalled bytes. VectorType.NewWithError() provides fast construction of slice type pointers, and extensive unit tests validate round-trip correctness, byte compatibility, nil/empty handling, dimension validation, special float values, buffer pool concurrency, and equivalence between fast and generic paths. Comprehensive benchmarks measure performance across element types and dimensions, including pooled variants.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 74.68% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and specifically summarizes the main change: optimization of vector marshal/unmarshal with concrete performance improvements for float32/float64/int32/int64.
Description check ✅ Passed The description comprehensively explains the changes, includes commit messages, detailed benchmark results, design decisions, and relationships to other PRs.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands.

@mykaul
mykaul force-pushed the vector-perf-optimize branch from c1b98db to 72fe318 Compare May 29, 2026 12:21
@mykaul

mykaul commented May 29, 2026

Copy link
Copy Markdown
Author

Review: vector marshal/unmarshal fast-path correctness (PR #770)

Reviewed standalone against origin/master with a deep focus on the vector fast-path bug classes seen in #744 and #871 (nil-vs-empty divergence, endianness). Rebased onto current master (clean, no conflicts) and re-ran the suite.

Findings

No HIGH or MEDIUM correctness issues found. The fast paths are well-implemented:

  • Endianness — CORRECT (top risk, clear). Every fast path uses binary.BigEndian.PutUint32/64 (marshal) and binary.BigEndian.Uint32/64 (unmarshal) on math.Float32bits/Float64bits and int32/int64. There is no unsafe byte-reinterpret / bulk memcpy of the slice — so it is correct on little-endian hosts. UUID elements use a plain copy of the [16]byte (no byte-swap needed, correct). The only unsafe.Pointer uses in marshal.go are pre-existing (duration/UUID conversions), not in the vector hot path.
  • nil-vs-empty — CORRECT (top risk, clear). This is the exact class that was HIGH in (improvement) perf: Vector float fast paths (subset of https://github.com/scylladb/gocql/pull/770 !) #744. Verified:
    • marshal: nil slice → nil bytes → framer writeBytes(nil) emits CQL null (-1); non-nil empty (dim=0) → non-nil zero-length make([]byte,0). Matches the generic path's make([]byte,0) and rv.IsNil() branches byte-for-byte.
    • unmarshal: data==nil*dst=nil; data non-nil & len 0 (dim=0) → non-nil empty slice, preserving the distinction; matches generic.
  • Bounds — CORRECT. Unmarshal validates len(data) == dim*elemSize exactly and rejects truncated/oversized input with a typed error; the _ = data[dim*N-1] lines are pure BCE hints that run only after the exact-length check, so no OOB.
  • Overflow — guarded. vectorByteSize rejects dim*elemBytes overflow (matters on 32-bit int).
  • Type dispatch — CORRECT. Only the exact concrete []float32/[]float64/[]int32/[]int64/[]UUID types take the fast path; named/aliased or other types fall through to the identical generic reflect path (confirmed by the new differential test, which relies on this fallthrough).
  • Pooling (vectorBufPool, conn.go) — SOUND. The write path is synchronous: c.exec calls buildFramewriteBytes (which copies via append(f.buf, p...)) → writeContext, all before c.exec returns. The executeQuery/executeBatch defers that call putVectorBuf fire only after c.exec returns, i.e. after the bytes are already copied and written. No aliasing/use-after-return; unmarshal slice-reuse copies fresh data into the reused backing array (no aliasing with data). -race on the vector tests (incl. TestVectorBufPool_Concurrency) is clean.
  • NaN / +Inf / -Inf / -0.0 / MinInt / MaxInt round-trips: covered by existing tests and the new differential test; pass.

Minor (non-blocking): a generic-path []float32-via-named-type marshal returns a non-pool bytes.Buffer slice that vectorBufPoolSubtype still routes to putVectorBuf. Harmless (buffer is unreferenced after the write; pool just gains a heap buffer). Not worth changing.

Changes made (test-only)

  • Added TestMarshalVector_FastPathMatchesGeneric (in marshal_vector_test.go): a differential test that forces the generic reflect path (via named slice types that fail the fast-path type assertions) and asserts byte-for-byte equality vs the fast path for float32/float64/int32/int64/UUID across nil, empty, single, many, NaN/Inf/-0.0, and Min/Max values, with an explicit nil-vs-empty assertion. No hot-path / production code was modified.

Test results

  • go build ./...go vet ./... ✅ (also with -tags unit)
  • go test -count=1 ./... → all pass (891).
  • go test -tags unit -count=1 . → 730 pass, 11 fail — all 11 are the known environmental PKI/TLS failures (Failed parsing or appending certs, missing generated testdata/pki/ca.crt): TestSSLSimple*, TestPolicyConnPoolSSL, TestShardAwarePortMocked* (TLS variants). Pre-existing, not regressions. No vector/marshal test fails.
  • go test -race -tags unit -run 'Vector|vector' . → all pass, no data races.
  • New differential test: 22/22 subtests pass.

Rebase / push

  • Rebased onto origin/master (which had moved ahead 2 commits, unrelated ring_describer/system.local changes). Clean, no conflicts. The vector fast-path commits applied unchanged.
  • Pushed: only the new commit is a content change and it is test-only, so per push policy I force-pushed (--force-with-lease) to mykaul/vector-perf-optimize (force needed only because the rebase rewrote the upstream perf commits onto newer master; their content is unchanged).

Benchmark recommendation (please run on a quiet machine)

The hot path itself was not modified by me, but since this PR is a perf change it should be benchmarked before merge:

go test -tags unit -run=^$ -bench Vector -benchmem -count=10 .

Compare against origin/master to confirm the claimed alloc/throughput wins (the fast paths claim zero-alloc steady state via vectorBufPool + slice reuse).

Overlap with #744

#770 and #744 touch the same vector fast-path territory; #744 is the zero-dimension subset. The nil-vs-empty handling here is consistent with the fix that #744 was addressing — the new differential test pins this behavior so a future change to either PR can't silently re-introduce the #744/#871 divergence.

mykaul added a commit to mykaul/gocql that referenced this pull request May 29, 2026
Verified all open Copilot findings on PR scylladb#770 against the current
rebased code. The marshal.go fast-path code already addresses the
correctness findings; only test improvements were needed.

Code findings — verified already correct, no marshal.go change:
- A (int overflow dim*4/dim*8): vectorByteSize computes int64(dim)*
  int64(elemBytes) with an overflow guard, and all marshal/unmarshal
  sizing + expected-length sites route through it; dim<0 is rejected.
- B (error type): vectorByteSize returns a plain error but every caller
  wraps it via marshalErrorf/unmarshalErrorf, so fast-path errors are
  proper MarshalError/UnmarshalError.
- C (nil-vs-empty): getVectorBuf(0) returns a non-nil make([]byte,0);
  nil vec -> nil bytes (CQL NULL), non-nil empty -> non-nil 0-len;
  unmarshal of 0-len non-nil data yields non-nil empty dst. Matches the
  generic path (covered by the differential test).
- E (GoDoc displacement): doc comments are adjacent to their symbols.
- F (vectorFixedElemSize switch): the helper is only a buf.Grow
  capacity hint in the generic path and is guarded by !isLengthType;
  excluded types are variable-length so unreachable. Doc already
  documents the deliberate exclusions.
- D (array dest validation): unreachable in the fast path — array dests
  (*[N]T) never match the slice-typed assertions and fall through to the
  generic reflect path, which already validates array length. Test added.

Test changes:
- G: replace fragile &slice[:1][0] backing-array-pointer capture with
  &slice[0] (the slices have len>0 here; [:1][0] panics on len-0).
- D: add TestUnmarshalVector_ArrayDestDimensionMismatch covering
  too-small/too-large/matching arrays and the dim==0 non-zero-array case.
- H (-0 sign bit): bit-level assertions via math.Float32bits/Float64bits
  for negative-zero already present in TestMarshalVector_SpecialValues.
@mykaul

mykaul commented May 29, 2026

Copy link
Copy Markdown
Author

Copilot review feedback — resolution summary

Reviewed all open Copilot findings against the current rebased code (branch tip 0d76cca8). The marshal.go fast-path code already addresses every correctness finding; only test-only improvements were required. No marshal.go hot-path changes were made.

Per-finding verdict

ID(s) Finding Verdict
A — 2936438432 (+441/449/456/461/464/467/475, 2937025239/245/254/261) int overflow on dim*4/dim*8 sizing & expected checks Already correct. All sites route through vectorByteSize, which computes int64(dim)*int64(elemBytes) and errors if negative or > math.MaxInt before alloc/slice; dim < 0 rejected up front.
B — 2937025220 vectorByteSize returns plain fmt.Errorf → wrong error type Already correct. Every caller wraps via marshalErrorf("%v", err) / unmarshalErrorf("%v", err), so type-asserting callers still get MarshalError/UnmarshalError.
C — 2937025229/239/245/254/261, 2942089314, 2942089284, 2968086682 nil-vs-empty (marshal/unmarshal) Already correct. getVectorBuf(0) returns non-nil make([]byte,0); nil vec→nil (NULL), non-nil empty→non-nil 0-len; unmarshal 0-len non-nil→non-nil empty dst. Matches generic path byte-for-byte. Covered by TestMarshalVector_FastPathMatchesGeneric, TestMarshalVector_EmptyVector (explicit data == nil assert), TestUnmarshalVectorFastPathZeroDimNonNilSlice (explicit dst == nil assert).
D — 2942089331 array dest (*[N]T) dim-mismatch may be skipped Not reachable in fast path; generic path correct. Array dests never match the slice-typed assertions and fall through to the generic reflect path, which validates array length in both the dim==0 and non-zero branches. Test added.
E — 2930751977, 2930969009 GoDoc comment displaced from isVectorVariableLengthType Already correct. Doc comments are adjacent to their symbols; each pool/helper has its own doc.
F — 2936438458 vectorFixedElemSize switch omits tinyint/smallint/date/time/counter Intentional. Helper is only a buf.Grow capacity hint in the generic path, guarded by !isLengthType; those types are variable-length so unreachable, and a 0 result just skips preallocation. Doc already documents the exclusions.
G — 2968086667/692/706/717 &slice[:1][0] backing-ptr capture panics on len-0 slice Fixed (test). Replaced with &slice[0] (slices have len>0 here; [:1][0] is fragile/incorrect).
H — 2930752034, 2930752052 -0 sign bit missed by reflect.DeepEqual Already correct. TestMarshalVector_SpecialValues asserts math.Float32bits/Float64bits for the negative-zero element.

Code vs test changes

  • Production code (marshal.go): none. All correctness findings verified already-correct or unreachable in the current code.
  • Tests (marshal_vector_test.go): G fix (&slice[:1][0]&slice[0], 2 sites) + D test TestUnmarshalVector_ArrayDestDimensionMismatch (too-small/too-large/matching arrays + dim==0-into-non-zero-array).

Test status

  • go build ./..., go vet ./...: clean.
  • go test -tags unit -count=1 .: 736 passed, 11 failed, 2 skipped — the 11 failures are the known environmental PKI/TLS fixture failures (Failed parsing or appending certs, missing testdata/pki/), unrelated to this PR. No other failures.
  • go test -tags unit -race -count=1 -run Vector .: 168 passed (no data races).

Push decision

Pushed (0d76cca8, force-with-lease). Per push policy, all changes are test-only (single file marshal_vector_test.go; no marshal.go hot-path change), so pushing without benchmarking is appropriate. Had any hot-path correctness change been required, it would have been held for benchmarking on an idle machine; that was not the case here.

(No marshal.go change → no re-benchmark required. If reviewers want a refresher anyway: go test -tags unit -run=^$ -bench Vector -benchmem -count=10 .)

mykaul added a commit to mykaul/gocql that referenced this pull request May 31, 2026
Verified all open Copilot findings on PR scylladb#770 against the current
rebased code. The marshal.go fast-path code already addresses the
correctness findings; only test improvements were needed.

Code findings — verified already correct, no marshal.go change:
- A (int overflow dim*4/dim*8): vectorByteSize computes int64(dim)*
  int64(elemBytes) with an overflow guard, and all marshal/unmarshal
  sizing + expected-length sites route through it; dim<0 is rejected.
- B (error type): vectorByteSize returns a plain error but every caller
  wraps it via marshalErrorf/unmarshalErrorf, so fast-path errors are
  proper MarshalError/UnmarshalError.
- C (nil-vs-empty): getVectorBuf(0) returns a non-nil make([]byte,0);
  nil vec -> nil bytes (CQL NULL), non-nil empty -> non-nil 0-len;
  unmarshal of 0-len non-nil data yields non-nil empty dst. Matches the
  generic path (covered by the differential test).
- E (GoDoc displacement): doc comments are adjacent to their symbols.
- F (vectorFixedElemSize switch): the helper is only a buf.Grow
  capacity hint in the generic path and is guarded by !isLengthType;
  excluded types are variable-length so unreachable. Doc already
  documents the deliberate exclusions.
- D (array dest validation): unreachable in the fast path — array dests
  (*[N]T) never match the slice-typed assertions and fall through to the
  generic reflect path, which already validates array length. Test added.

Test changes:
- G: replace fragile &slice[:1][0] backing-array-pointer capture with
  &slice[0] (the slices have len>0 here; [:1][0] panics on len-0).
- D: add TestUnmarshalVector_ArrayDestDimensionMismatch covering
  too-small/too-large/matching arrays and the dim==0 non-zero-array case.
- H (-0 sign bit): bit-level assertions via math.Float32bits/Float64bits
  for negative-zero already present in TestMarshalVector_SpecialValues.
@mykaul
mykaul force-pushed the vector-perf-optimize branch from 0d76cca to 82b3a75 Compare May 31, 2026 10:54
@mykaul
mykaul marked this pull request as ready for review June 5, 2026 10:02
@mykaul

mykaul commented Jun 5, 2026

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
conn.go (1)

1693-1724: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Install pooled-buffer cleanup before marshalling loops.

On Line 1693 and Line 1905 paths, cleanup is deferred only after marshalling completes. If marshalling fails mid-loop, already-created vector buffers are not returned to the pool.

♻️ Proposed fix
@@ func (c *Conn) executeQuery(ctx context.Context, qry *Query) (iter *Iter) {
-		params.values = make([]queryValues, len(values))
-		for i := 0; i < len(values); i++ {
+		cols := info.request.columns
+		params.values = make([]queryValues, len(values))
+
+		hasPooledVec := false
+		for i := 0; i < len(values); i++ {
+			if vt, ok := cols[i].TypeInfo.(VectorType); ok && vectorBufPoolSubtype(vt) {
+				hasPooledVec = true
+				break
+			}
+		}
+		if hasPooledVec {
+			defer func() {
+				for i := 0; i < len(params.values) && i < len(cols); i++ {
+					if vt, ok := cols[i].TypeInfo.(VectorType); ok && vectorBufPoolSubtype(vt) {
+						putVectorBuf(params.values[i].value)
+					}
+				}
+			}()
+		}
+
+		for i := 0; i < len(values); i++ {
 			v := &params.values[i]
 			value := values[i]
-			typ := info.request.columns[i].TypeInfo
+			typ := cols[i].TypeInfo
 			if err := marshalQueryValue(typ, value, v); err != nil {
 				return &Iter{err: err}
 			}
 		}
-
-		cols := info.request.columns
-		vals := params.values
-		hasPooledVec := false
-		for _, col := range cols {
-			if vt, ok := col.TypeInfo.(VectorType); ok && vectorBufPoolSubtype(vt) {
-				hasPooledVec = true
-				break
-			}
-		}
-		if hasPooledVec {
-			defer func() {
-				for i, col := range cols {
-					if vt, ok := col.TypeInfo.(VectorType); ok && vectorBufPoolSubtype(vt) {
-						putVectorBuf(vals[i].value)
-					}
-				}
-			}()
-		}
@@ func (c *Conn) executeBatch(ctx context.Context, batch *Batch) (iter *Iter) {
 	var vectorBufs [][]byte
+	defer func() {
+		for _, buf := range vectorBufs {
+			putVectorBuf(buf)
+		}
+	}()
@@
-	if len(vectorBufs) > 0 {
-		defer func() {
-			for _, buf := range vectorBufs {
-				putVectorBuf(buf)
-			}
-		}()
-	}

Also applies to: 1905-1971

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@conn.go` around lines 1693 - 1724, The pooled-vector cleanup defer must be
installed before the marshalling loop so partially-marshalled vector buffers are
returned on error: detect whether any column TypeInfo is a poolable VectorType
(use info.request.columns, VectorType and vectorBufPoolSubtype) immediately
after allocating params.values, then, if true, install a defer that iterates
over captured cols/vals and calls putVectorBuf(vals[i].value); only after that
call marshalQueryValue for each value (the loop that currently returns
&Iter{err: err} on failure) so any buffers allocated prior to an error are
released; ensure the defer captures the same params.values slice used by the
marshalling loop to avoid missing entries.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@conn.go`:
- Around line 1693-1724: The pooled-vector cleanup defer must be installed
before the marshalling loop so partially-marshalled vector buffers are returned
on error: detect whether any column TypeInfo is a poolable VectorType (use
info.request.columns, VectorType and vectorBufPoolSubtype) immediately after
allocating params.values, then, if true, install a defer that iterates over
captured cols/vals and calls putVectorBuf(vals[i].value); only after that call
marshalQueryValue for each value (the loop that currently returns &Iter{err:
err} on failure) so any buffers allocated prior to an error are released; ensure
the defer captures the same params.values slice used by the marshalling loop to
avoid missing entries.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4af3cac7-ef8e-4e7a-899e-fb458cbb3084

📥 Commits

Reviewing files that changed from the base of the PR and between bed2bb0 and 82b3a75.

📒 Files selected for processing (7)
  • conn.go
  • helpers_bench_test.go
  • marshal.go
  • marshal_test.go
  • marshal_vector_test.go
  • tests/bench/bench_vector_public_test.go
  • vector_bench_test.go

@mykaul
mykaul marked this pull request as draft June 5, 2026 10:15
mykaul added a commit to mykaul/gocql that referenced this pull request Jun 5, 2026
Verified all open Copilot findings on PR scylladb#770 against the current
rebased code. The marshal.go fast-path code already addresses the
correctness findings; only test improvements were needed.

Code findings — verified already correct, no marshal.go change:
- A (int overflow dim*4/dim*8): vectorByteSize computes int64(dim)*
  int64(elemBytes) with an overflow guard, and all marshal/unmarshal
  sizing + expected-length sites route through it; dim<0 is rejected.
- B (error type): vectorByteSize returns a plain error but every caller
  wraps it via marshalErrorf/unmarshalErrorf, so fast-path errors are
  proper MarshalError/UnmarshalError.
- C (nil-vs-empty): getVectorBuf(0) returns a non-nil make([]byte,0);
  nil vec -> nil bytes (CQL NULL), non-nil empty -> non-nil 0-len;
  unmarshal of 0-len non-nil data yields non-nil empty dst. Matches the
  generic path (covered by the differential test).
- E (GoDoc displacement): doc comments are adjacent to their symbols.
- F (vectorFixedElemSize switch): the helper is only a buf.Grow
  capacity hint in the generic path and is guarded by !isLengthType;
  excluded types are variable-length so unreachable. Doc already
  documents the deliberate exclusions.
- D (array dest validation): unreachable in the fast path — array dests
  (*[N]T) never match the slice-typed assertions and fall through to the
  generic reflect path, which already validates array length. Test added.

Test changes:
- G: replace fragile &slice[:1][0] backing-array-pointer capture with
  &slice[0] (the slices have len>0 here; [:1][0] panics on len-0).
- D: add TestUnmarshalVector_ArrayDestDimensionMismatch covering
  too-small/too-large/matching arrays and the dim==0 non-zero-array case.
- H (-0 sign bit): bit-level assertions via math.Float32bits/Float64bits
  for negative-zero already present in TestMarshalVector_SpecialValues.
@mykaul
mykaul force-pushed the vector-perf-optimize branch from 82b3a75 to addfc51 Compare June 5, 2026 11:11
@mykaul

mykaul commented Jun 5, 2026

Copy link
Copy Markdown
Author

Update — addressed CodeRabbit finding

🟡 Minor (outside-diff) — pooled vector buffers leak on mid-loop marshal failure (conn.go)

In both executeQuery and executeBatch, the putVectorBuf cleanup defer was installed after the marshalling loop. If marshalQueryValue failed mid-loop (return &Iter{err: err}), the defer was never registered and any vector buffers already marshalled leaked from vectorBufPool.

  • executeQuery: moved the hasPooledVec detection + cleanup defer to before the marshalling loop. The defer iterates params.values with bounds guards; entries not yet marshalled hold a nil value and putVectorBuf(nil) is a no-op.
  • executeBatch: installed the vectorBufs cleanup defer immediately after the slice is declared (before the loop). The closure reads vectorBufs at function exit, so it returns whatever was accumulated before an early return.

Timing is unchanged for the success path (defer still runs at function exit, after c.exec/buildFrame copies the bytes). Note: the batch defer is now unconditional rather than gated on len(vectorBufs) > 0 — the leak-safety outweighs the negligible defer cost on vector-free batches.

go build, go vet, gofmt, and the full unit suite pass. Squashed into the "wire putVectorBuf into connection write path" commit; rebased on origin/master and force-pushed.

mykaul added a commit to mykaul/gocql that referenced this pull request Jun 7, 2026
…-reuse tests

float32 and int32 subtests already had an explicit length check before
taking the backing-array pointer (&result[0]). The float64 and int64
subtests were missing those guards, so a hypothetical bug in
unmarshalVectorFloat64/Int64 that returned nil error without populating
the destination would silently panic instead of producing a diagnostic
failure.

Add the same 'if len(result) != dim { t.Fatalf(...) }' checks for
consistency and defensive test quality (addresses Copilot round-4
finding on PR scylladb#770).
@mykaul

mykaul commented Jun 7, 2026

Copy link
Copy Markdown
Author

@copilot-pull-request-reviewer Thanks for the review across 6 rounds. Here is how every finding was addressed, all in current HEAD (4812592):

Panic - &result[:1][0] on zero-length slices (IDs 2930969041/68/90/108, 2968086667/692): Fixed in addfc51 - replaced with &result[0]. Commit 4812592 adds the missing len checks in float64 and int64 subtests too.

dim*N integer overflow (IDs 2936438432/41/49/56/61/64/67/75): All sizing routes through vectorByteSize(dim, N) using int64 arithmetic with overflow guard. BCE hints are only reached after that check and dim>0 guard.

vectorByteSize error type (ID 2937025220): Every call site wraps via marshalErrorf / unmarshalErrorf.

getVectorBuf(0) returning nil (IDs 2937025229, 2942089314): Fixed in 62a7f4a - returns make([]byte, 0) for size==0.

dim==0 fast paths leaving *dst nil (IDs 2937025239/45/54/61): Fixed in 62a7f4a - all fast-path unmarshal functions handle dim==0 explicitly.

Generic marshal returns nil for dim==0 (ID 2968086682): Fixed in 62a7f4a - early 'if n == 0 { return make([]byte, 0), nil }' guard added.

Empty vector tests pass for nil (ID 2942089284): Already checked - TestMarshalVector_EmptyVector asserts data != nil explicitly.

dim==0 array destination validation (ID 2942089331): Fast paths only match *[]T; array destinations fall through to generic path which validates array length. TestUnmarshalVector_ArrayDestDimensionMismatch covers this.

Generic unmarshal divide-by-zero (ID 2930752007): Guarded by 'if info.Dimensions == 0' at line 971, before the division.

reflect.DeepEqual / -0.0 (IDs 2930752034/52): TestMarshalVector_SpecialValues already has explicit bit-level math.Float32bits/Float64bits checks.

Doc comment placement (IDs 2930751977, 2930969009) and vectorFixedElemSize comment (ID 2936438458): All accurate in current code, no change needed.

mykaul added a commit to mykaul/gocql that referenced this pull request Aug 19, 2026
Verified all open Copilot findings on PR scylladb#770 against the current
rebased code. The marshal.go fast-path code already addresses the
correctness findings; only test improvements were needed.

Code findings — verified already correct, no marshal.go change:
- A (int overflow dim*4/dim*8): vectorByteSize computes int64(dim)*
  int64(elemBytes) with an overflow guard, and all marshal/unmarshal
  sizing + expected-length sites route through it; dim<0 is rejected.
- B (error type): vectorByteSize returns a plain error but every caller
  wraps it via marshalErrorf/unmarshalErrorf, so fast-path errors are
  proper MarshalError/UnmarshalError.
- C (nil-vs-empty): getVectorBuf(0) returns a non-nil make([]byte,0);
  nil vec -> nil bytes (CQL NULL), non-nil empty -> non-nil 0-len;
  unmarshal of 0-len non-nil data yields non-nil empty dst. Matches the
  generic path (covered by the differential test).
- E (GoDoc displacement): doc comments are adjacent to their symbols.
- F (vectorFixedElemSize switch): the helper is only a buf.Grow
  capacity hint in the generic path and is guarded by !isLengthType;
  excluded types are variable-length so unreachable. Doc already
  documents the deliberate exclusions.
- D (array dest validation): unreachable in the fast path — array dests
  (*[N]T) never match the slice-typed assertions and fall through to the
  generic reflect path, which already validates array length. Test added.

Test changes:
- G: replace fragile &slice[:1][0] backing-array-pointer capture with
  &slice[0] (the slices have len>0 here; [:1][0] panics on len-0).
- D: add TestUnmarshalVector_ArrayDestDimensionMismatch covering
  too-small/too-large/matching arrays and the dim==0 non-zero-array case.
- H (-0 sign bit): bit-level assertions via math.Float32bits/Float64bits
  for negative-zero already present in TestMarshalVector_SpecialValues.
mykaul added a commit to mykaul/gocql that referenced this pull request Aug 19, 2026
…-reuse tests

float32 and int32 subtests already had an explicit length check before
taking the backing-array pointer (&result[0]). The float64 and int64
subtests were missing those guards, so a hypothetical bug in
unmarshalVectorFloat64/Int64 that returned nil error without populating
the destination would silently panic instead of producing a diagnostic
failure.

Add the same 'if len(result) != dim { t.Fatalf(...) }' checks for
consistency and defensive test quality (addresses Copilot round-4
finding on PR scylladb#770).
@mykaul
mykaul force-pushed the vector-perf-optimize branch from 4812592 to ec02e89 Compare August 19, 2026 17:40
mykaul added 7 commits August 24, 2026 12:36
Add type-specialized fast paths for vector<float>, vector<double>,
vector<int>, and vector<bigint> that bypass reflect-based per-element
marshaling in favor of direct encoding/binary bulk conversion.

Changes in marshal.go:
- Type switches in marshalVector()/unmarshalVector() dispatch to
  dedicated functions for []float32, []float64, []int32, []int64
  before falling through to the generic reflect path.
- 8 new functions: marshalVectorFloat32, marshalVectorFloat64,
  unmarshalVectorFloat32, unmarshalVectorFloat64, marshalVectorInt32,
  marshalVectorInt64, unmarshalVectorInt32, unmarshalVectorInt64.
- sync.Pool buffer reuse (vectorBufPool/getVectorBuf/putVectorBuf)
  for zero-alloc steady state when callers return buffers after
  the framer copies them. 64KiB cap prevents pool bloat.
- Unmarshal fast paths reuse destination slice backing array when
  capacity is sufficient (zero-alloc steady state on read path).
- Generic path preallocation via vectorFixedElemSize() + buf.Grow()
  for non-fast-path fixed-size types (e.g. UUID, timestamp).
- vectorByteSize() helper guards against integer overflow on 32-bit
  platforms with corrupt or adversarial schema metadata.
- All fast-path errors are wrapped as MarshalError/UnmarshalError
  for consistent error typing.
- dim=0 vectors correctly encode as non-nil empty values (not CQL NULL)
  in both fast paths and generic path.
- Negative dimensions are rejected with clear error messages.

Benchmark results for vector<float, 1536> (typical embedding dimension):

  Marshal (baseline -> optimized):
    86.4 us/op  ->  3.4 us/op  (25x faster)
    3081 allocs ->  2 allocs    (99.94% fewer)
    28632 B/op  ->  6172 B/op   (78% less memory)

  Marshal with pool return (steady state):
    86.4 us/op  ->  1.6 us/op  (54x faster)
    3081 allocs ->  2 allocs    (99.94% fewer)
    28632 B/op  ->  48 B/op     (99.8% less memory)

  Unmarshal (baseline -> optimized):
    60.2 us/op  ->  1.5 us/op  (41x faster)
    2 allocs    ->  0 allocs    (100% fewer)
    6168 B/op   ->  0 B/op      (100% less memory)

  Round-trip (baseline -> optimized, pooled):
    147.8 us/op ->  3.1 us/op  (48x faster)
    3083 allocs ->  2 allocs    (99.94% fewer)
    34800 B/op  ->  48 B/op     (99.9% less memory)

  Throughput: 80 MB/s -> 3.5 GB/s (geomean, +2900%)

New test files:
- marshal_vector_test.go: 58+ unit subtests across 13 categories
  (round-trip, byte-compat, slice-reuse, nil, dimension-mismatch,
  empty-vector, pointer-to-slice, special-values, pool-concurrency,
  oversized-not-pooled, fixed-elem-size, generic-prealloc).
- vector_bench_test.go: extended with int32/int64 and pooled benchmarks.
- tests/bench/bench_vector_public_test.go: public API benchmarks for
  int32/int64 marshal/unmarshal.

Subsumes PR scylladb#744 (float fast paths) and PR scylladb#745 (generic prealloc).
Extends with int32/int64 fast paths and buffer pooling not covered by
any existing PR.
Return pooled vector buffers to vectorBufPool after the framer copies
marshalled bytes in executeQuery and executeBatch. This completes the
zero-alloc steady-state cycle for vector marshal operations.

In executeQuery, a defer after the marshal loop returns buffers for
columns identified as pooled vector types (float32, float64, int32,
int64). In executeBatch, vector buffers are collected across all batch
statements and returned via a single defer.

The vectorBufPoolSubtype helper centralizes the type check to keep
the two call sites consistent with the marshal fast paths.

Includes unit tests covering vectorBufPoolSubtype classification,
single-query and batch pool return simulation, and non-pooled type
safety.
Add dedicated marshal/unmarshal fast paths for UUID and TimeUUID vector
elements, following the same pattern as the existing float32/float64/
int32/int64 fast paths.

UUID is [16]byte with no endian conversion needed, so the fast path
uses a simple copy() loop. Uses pooled buffers via getVectorBuf for
zero-alloc steady state on the marshal path, and reuses the destination
slice backing array on the unmarshal path.

Benchmarks (vs generic reflection path):
- Marshal: ~90% faster (10x speedup), 99%+ fewer allocations
- Unmarshal: ~97% faster (30-35x speedup), zero allocations
- Marshal+pool: additional 4x over non-pooled marshal
…arse

VectorType embeds NativeType but had no NewWithError() method, so calls
fell through to NativeType.NewWithError() which hit the TypeCustom
fallback: goType() → asVectorType() → re-parse the full Java type string
(e.g. 'org.apache.cassandra.db.marshal.VectorType(FloatType, 1536)')
on every invocation. This is called per-column per-row by RowData() and
MapScan, making it a hot path for vector workloads.

Add VectorType.NewWithError() with fast paths for all common element
types (float32, float64, int32, int64, UUID, string, bool, etc.) that
return *[]T directly without reflection or string parsing. Fallback for
exotic subtypes still uses SubType.NewWithError() + reflect.SliceOf but
avoids the asVectorType() re-parse.

Also fix zero-dimension error messages in fast-path unmarshal functions
to be consistent with the generic path (check dim==0 before byte-size
validation), fix copyright header in marshal_vector_test.go, and fix
pre-existing session_unit_test.go build error from origin/master
(hostId string → UUID type mismatch).

Benchmark results (VectorType.NewWithError vs NativeType fallback):

  VectorType:          ~17 ns/op, 24 B/op, 1 allocs/op
  NativeType_fallback: ~170 ns/op, 92 B/op, 4 allocs/op

  → 10x faster, 75% fewer allocations
…or vectors

Adds TestMarshalVector_FastPathMatchesGeneric which forces the generic
reflect-based marshal path (via named slice types that fail the fast-path
type assertions) and compares its output byte-for-byte against the optimized
fast path for float32/float64/int32/int64/UUID vectors across nil, empty,
single, many, NaN/Inf/negative-zero, and min/max-value cases.

Specifically guards the nil-vs-empty distinction (nil -> nil bytes / CQL null;
empty -> non-nil zero-length) which was the HIGH-severity bug class in scylladb#744/scylladb#871.

Test-only change.
Verified all open Copilot findings on PR scylladb#770 against the current
rebased code. The marshal.go fast-path code already addresses the
correctness findings; only test improvements were needed.

Code findings — verified already correct, no marshal.go change:
- A (int overflow dim*4/dim*8): vectorByteSize computes int64(dim)*
  int64(elemBytes) with an overflow guard, and all marshal/unmarshal
  sizing + expected-length sites route through it; dim<0 is rejected.
- B (error type): vectorByteSize returns a plain error but every caller
  wraps it via marshalErrorf/unmarshalErrorf, so fast-path errors are
  proper MarshalError/UnmarshalError.
- C (nil-vs-empty): getVectorBuf(0) returns a non-nil make([]byte,0);
  nil vec -> nil bytes (CQL NULL), non-nil empty -> non-nil 0-len;
  unmarshal of 0-len non-nil data yields non-nil empty dst. Matches the
  generic path (covered by the differential test).
- E (GoDoc displacement): doc comments are adjacent to their symbols.
- F (vectorFixedElemSize switch): the helper is only a buf.Grow
  capacity hint in the generic path and is guarded by !isLengthType;
  excluded types are variable-length so unreachable. Doc already
  documents the deliberate exclusions.
- D (array dest validation): unreachable in the fast path — array dests
  (*[N]T) never match the slice-typed assertions and fall through to the
  generic reflect path, which already validates array length. Test added.

Test changes:
- G: replace fragile &slice[:1][0] backing-array-pointer capture with
  &slice[0] (the slices have len>0 here; [:1][0] panics on len-0).
- D: add TestUnmarshalVector_ArrayDestDimensionMismatch covering
  too-small/too-large/matching arrays and the dim==0 non-zero-array case.
- H (-0 sign bit): bit-level assertions via math.Float32bits/Float64bits
  for negative-zero already present in TestMarshalVector_SpecialValues.
…-reuse tests

float32 and int32 subtests already had an explicit length check before
taking the backing-array pointer (&result[0]). The float64 and int64
subtests were missing those guards, so a hypothetical bug in
unmarshalVectorFloat64/Int64 that returned nil error without populating
the destination would silently panic instead of producing a diagnostic
failure.

Add the same 'if len(result) != dim { t.Fatalf(...) }' checks for
consistency and defensive test quality (addresses Copilot round-4
finding on PR scylladb#770).
@mykaul
mykaul force-pushed the vector-perf-optimize branch from ec02e89 to 260ac41 Compare August 24, 2026 09:41
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.

3 participants