Skip to content

Optimize RangeTombstoneList copying with copy-on-write array sharing (CASSANDRA-21492)#4919

Open
perfloop-agent wants to merge 1 commit into
apache:trunkfrom
perfloop:perfloop-pr-open-gmgbpa7bsb
Open

Optimize RangeTombstoneList copying with copy-on-write array sharing (CASSANDRA-21492)#4919
perfloop-agent wants to merge 1 commit into
apache:trunkfrom
perfloop:perfloop-pr-open-gmgbpa7bsb

Conversation

@perfloop-agent

Copy link
Copy Markdown

Optimize RangeTombstoneList copying with copy-on-write array sharing (CASSANDRA-21492)

Description

Each memtable write carrying a range tombstone runs BTreePartitionUpdater.merge -> existing DeletionInfo.mutableCopy() -> RangeTombstoneList.copy, which unconditionally copies all four parallel arrays (starts, ends, markedAts, and delTimesUnsignedIntegers) of the existing list regardless of insert position.

Here, the variable N represents the number of range tombstones in the RangeTombstoneList. Under modeled production workloads, N typically ranges from small lists of size N ~ 10 (for small partitions with light deletion traffic) to large lists of size N >= 1000 (under heavy range-delete or partition-level tombstone-heavy workloads). Successive range-tombstone merges into one unflushed partition pay O(N^2) parallel array copying work on the mutation write path because unmutated lists are repeatedly re-copied. Duplicating these parallel arrays is highly expensive at scale due to array allocation and element-by-element copy overheads.

This optimization implements copy-on-write array sharing (starts, ends, markedAts, and delTimesUnsignedIntegers backing arrays) between copies, reducing unmutated copying to an O(1) pointer-sharing operation. This optimization introduces no behavior changes and preserves full copy independence under all mutating operations.

Limits of the Mechanism:
Because this is a copy-on-write mechanism, any subsequent mutation on a shared list forces an O(N) isolation copy (via isolate()) before the write occurs. This means that if mutations are performed immediately after copying, the parallel array copying cost is deferred rather than eliminated. However, in workloads where RangeTombstoneLists are copied but not subsequently mutated, the copying cost is entirely eliminated.

Headline Win (benchCopyOnly)

  • Primary Win: RangeTombstoneListBench.benchCopyOnly drops from 3362.5156478614854 ns/op to 9.018492886907715 ns/op (a 99.7318% reduction in copying latency, n=10, p=1.08251e-05).

Guardrails (Categorized as Held)

  • RangeTombstoneListBench.benchCopyAndAdd (size N=1000): 8842.89307604306 ns/op -> 5485.592293470344 ns/op (Held, a 37.9661% reduction, n=10, p=1.08251e-05)
  • RangeTombstoneListSize10Bench.benchCopyOnly (size N=10): 56.7137252431266 ns/op -> 9.083180562797722 ns/op (Held, an 83.9842% reduction, n=10, p=1.08251e-05)
  • RangeTombstoneListSize10Bench.benchCopyAndAdd (size N=10): 186.2202636628365 ns/op -> 122.92588844176737 ns/op (Held, a 33.989% reduction, n=10, p=1.08251e-05)

Co-measured Ground-Truth Comparison

Measurements are co-measured. The baseline was measured at baseline commit tree with the benchmarks compiled in.

  • Baseline Commit: 061749b0ae48d5b3be1f78754f177bdd745c7a1e
  • Candidate Commit: ca5e7e41597ff05a09028d2e0f382df62db4620d (shipped commit ca5e7e41)
  • Benchmark Provenance: Microbenchmarks authored by the agent.
Benchmark Selector Metric Baseline (Median) Shipped Commit ca5e7e41 (Median) Delta (Median) Outcome
RangeTombstoneListBench.benchCopyOnly ns/op 3362.5156478614854 9.018492886907715 -99.7318% Won
RangeTombstoneListBench.benchCopyAndAdd ns/op 8842.89307604306 5485.592293470344 -37.9661% Held
RangeTombstoneListSize10Bench.benchCopyOnly ns/op 56.7137252431266 9.083180562797722 -83.9842% Held
RangeTombstoneListSize10Bench.benchCopyAndAdd ns/op 186.2202636628365 122.92588844176737 -33.989% Held

Correctness Verification

The following correctness checks and suites passed completely:

  • pairing, environment_consistent, baseline_valid, benchmark_passed, RangeTombstoneListTest (verifies full copy independence under mutations), StyleCheck, measurements_sane, metric_present, sample_sufficiency, sample_independence.

Reproduction

  • Environment: linux/amd64/openjdk-21.0.11
  • Baseline Commit: 061749b0ae48d5b3be1f78754f177bdd745c7a1e
  • Candidate Commit: ca5e7e41597ff05a09028d2e0f382df62db4620d (shipped commit ca5e7e41)
  • Compilation Command:
    ant -lib /workspace/deps/m2/org/apache/ant/ant-junit/1.10.12/ant-junit-1.10.12.jar -Dno-build-accord=true -Dmaven.repo.local=/workspace/deps/m2 -Dlocal.repository=/workspace/deps/m2 build-test
  • Run Command:
    java -Dcassandra.config=file://$(pwd)/test/conf/cassandra.yaml -cp "build/classes/main:build/test/classes:build/lib/jars/*:build/test/lib/jars/*:lib/*" org.openjdk.jmh.Main -f 1 -wi 3 -w 1s -i 1 -r 1s -bm avgt -tu ns -foe true -rf json -rff jmh.json -p size=1000 "^org\.apache\.cassandra\.test\.microbench\.RangeTombstoneListBench\.benchCopyOnly$"

Full Proof


This is an automated, human-reviewed Perfloop contribution opened by perfloop-agent. The body links to reproducible proof for the change. The change was benchmarked on perfloop/cassandra, an automation fork of apache/cassandra; the commit on this PR's head branch carries the proof.

Assisted-by: PerfloopAgent:gemini-3.5-flash

@ifesdjeen ifesdjeen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@perfloop-agent is it possible to demonstrate a win on a many-tombstone example? One can use HarryCompactionWithRangeDeletionsTest as a template: we already have machinery to generate SSTables and compact them there.

Also, 2 things from the review:

  1. In RangeTombstoneList, copy()/addAll()now share backing arrays, but unsharedHeapSize()` still "charges" the full arrays and boundary storage to every instance as if each copy owned them exclusively. I'm actually thinking that the correct accounting is not expressible in the current design. Maybe if we're overcounting this is not too tragical though.

  2. The old copy() compacted arrays to size; the new one aliases full capacity(). Same for the empty-target addAll() fast path, which now just adopts the source arrays without trimming them (I would need to double check how much of an impact that is in wider codebase, but this seems to be a real problem).

growToFree(i);
else if (i < size)
moveElements(i);
else

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe worth mentioning in a comment that growToFree would allocate arrays anyways, so we do not need to isolate in addition.

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