Skip to content

perf: eliminate heap allocations in gocql wrapper chainable methods#9621

Open
mykaul wants to merge 2 commits intotemporalio:mainfrom
mykaul:perf/gocql-reduce-heap-allocations
Open

perf: eliminate heap allocations in gocql wrapper chainable methods#9621
mykaul wants to merge 2 commits intotemporalio:mainfrom
mykaul:perf/gocql-reduce-heap-allocations

Conversation

@mykaul
Copy link
Contributor

@mykaul mykaul commented Mar 23, 2026

Summary

  • Eliminate heap allocations in gocql query wrapper chainable methods (Consistency, WithContext, PageSize, etc.) by mutating in-place and returning *queryImpl instead of allocating new wrapper objects.
  • Apply the same optimization to gocql batch wrapper chainable methods (WithContext, WithTimestamp).

Motivation

The gocql query/batch wrappers use a fluent (method chaining) pattern where each method previously created a new queryImpl/batchImpl on the heap. Since every Cassandra query goes through these wrappers, this creates significant allocation pressure on the hot path — each query allocates 3-5 wrapper objects that are immediately discarded.

Approach

Changed chainable methods from copy-on-return to mutate-in-place semantics. This is safe because all callers use method chaining (e.g., q.Consistency(x).WithContext(ctx).PageSize(n)) and never retain intermediate copies.

Before:

func (q *queryImpl) Consistency(c gocql.Consistency) Query {
    return &queryImpl{query: q.query.Consistency(c)}  // heap alloc
}

After:

func (q *queryImpl) Consistency(c gocql.Consistency) Query {
    q.query.Consistency(c)  // mutate in place
    return q                // no alloc
}

Benchmark Results (Cassandra 5.0, throughput_stress scenario)

Commit Iterations (5min) vs Baseline (1027)
Query wrapper alloc elimination 1080 +5.6%
Batch wrapper alloc elimination 1077 +5.3%

Testing

  • Existing gocql wrapper unit tests pass with -race.

mykaul added 2 commits March 23, 2026 09:58
All chainable methods on the query wrapper (Consistency, WithTimestamp,
WithContext, PageSize, PageState, Idempotent) previously allocated a
new *query struct on every call. Upstream gocql mutates in place for
all methods except WithContext (which does a shallow copy). Mirror
this by mutating and returning the receiver directly, removing one
heap allocation per call site in the persistence hot path.

Note: WithContext mutates the receiver in place (via the underlying
gocql shallow copy behavior), unlike gocql's method signature which
suggests a copy.
Same pattern as query.go: mutate and return the receiver instead
of allocating a new *batch on each chainable call. WithContext
mutates in place via gocql's underlying shallow copy.
@mykaul mykaul requested review from a team as code owners March 23, 2026 07:58
@mykaul
Copy link
Contributor Author

mykaul commented Mar 23, 2026

I've tested this extensively with ScyllaDB 2026.1, Cassandra 5.0 and Cassandra 3.11, running multiple Omes workloads.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant