Skip to content

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

Open
koo-taejin wants to merge 2 commits into
apache:trunkfrom
koo-taejin:CASSANDRA-21535-trunk
Open

[CASSANDRA-21535] Avoid extra copy for cached Mutation serialization#4965
koo-taejin wants to merge 2 commits into
apache:trunkfrom
koo-taejin:CASSANDRA-21535-trunk

Conversation

@koo-taejin

Copy link
Copy Markdown
Contributor

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.

patch by koo.taejin; reviewed by for CASSANDRA-21535

The Cassandra Jira

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.
@netudima

Copy link
Copy Markdown
Contributor

Could you please also share the JMH benchmark too?

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.
@koo-taejin

Copy link
Copy Markdown
Contributor Author

I also include the files used for the test.

Sorry, I should explain the test environment and the test result more clearly. I shared the result without including enough test details.

Please feel free to give me any feedback.

Thank you. :)

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.

2 participants