[CASSANDRA-21535] Avoid extra copy for cached Mutation serialization

Description
In Mutation.Serializer.serialization(), we first calculate the serialized size.

After that, current code serializes the mutation to a thread local DataOutputBuffer.
Then unsafeToByteArray() creates a new byte array and copies all serialized bytes.

The serialized size is already known.
So we can create a fixed size heap buffer first and serialize directly to that buffer.

This change removes one full byte array copy when we create a cached mutation serialization.

I also added a check that the written size is same as the calculated serialized size.

Benchmark
I added a JMH benchmark for this case.

For every benchmark operation:

  - clear cached serialization
  - serialize the Mutation
  - create cached serialization again

JDK 17, arm64, 1 thread:

  | Value size | Before | After | Result |
  |--:|-:|-:|--:|
  | 128 B | 3.70 M ops/s | 3.87 M ops/s | +4% |
  | 16 KiB | 608 K ops/s | 716 K ops/s | +18% |
  | 256 KiB | 41.4 K ops/s | 53.1 K ops/s | +28% |
  | 768 KiB | 6.83 K ops/s | 18.88 K ops/s | +177% |

The result is bigger for large mutations because current code writes the data to a
direct buffer first, and then copies all data again to a heap byte array.

With this change, serialization writes directly to the final heap byte array.

This benchmark measures cache creation cost. It does not mean normal write throughput will be 2.77x faster, because normal requests can reuse the cached serialization.
This commit is contained in:
koo.taejin 2026-07-26 16:41:53 +09:00
parent db0871ce8b
commit 0bd57f16f6
1 changed files with 6 additions and 2 deletions

View File

@ -48,6 +48,7 @@ import org.apache.cassandra.db.rows.DeserializationHelper;
import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.DataOutputBuffer;
import org.apache.cassandra.io.util.DataOutputBufferFixed;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.io.util.TeeDataInputPlus;
import org.apache.cassandra.locator.ReplicaPlan;
@ -537,10 +538,13 @@ public class Mutation implements IMutation, Supplier<Mutation>
// so we only cache serialized mutations when they are below the defined limit.
if (serializedSize < CACHEABLE_MUTATION_SIZE_LIMIT)
{
try (DataOutputBuffer dob = DataOutputBuffer.scratchBuffer.get())
int serializedSizeAsInt = (int) serializedSize;
try (DataOutputBufferFixed dob = new DataOutputBufferFixed(serializedSizeAsInt))
{
serializeInternal(PartitionUpdate.serializer, mutation, dob, version);
serialization = new CachedSerialization(dob.unsafeToByteArray());
checkState(dob.getLength() == serializedSizeAsInt,
"Expected serialized size %s but got %s", serializedSize, dob.getLength());
serialization = new CachedSerialization(dob.getData());
}
catch (IOException e)
{