From 0bd57f16f6e2b998af2efc92b1184ee812ab708a Mon Sep 17 00:00:00 2001 From: "koo.taejin" Date: Sun, 26 Jul 2026 16:41:53 +0900 Subject: [PATCH] [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. --- src/java/org/apache/cassandra/db/Mutation.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/java/org/apache/cassandra/db/Mutation.java b/src/java/org/apache/cassandra/db/Mutation.java index 9fc49ae15e..07a36372ce 100644 --- a/src/java/org/apache/cassandra/db/Mutation.java +++ b/src/java/org/apache/cassandra/db/Mutation.java @@ -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 // 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) {