Skip to content

Autoresearch/perf optimization 20260320 - #6

Draft
jahfer wants to merge 46 commits into
mainfrom
autoresearch/perf-optimization-20260320
Draft

Autoresearch/perf optimization 20260320#6
jahfer wants to merge 46 commits into
mainfrom
autoresearch/perf-optimization-20260320

Conversation

@jahfer

@jahfer jahfer commented Mar 20, 2026

Copy link
Copy Markdown
Owner

No description provided.

jahfer added 30 commits January 14, 2026 23:13
Replace heap-allocated AtomicPtr<Marker<K,V>> with inline atomic fields
(AtomicU8 for state, AtomicIsize for move destination). The K,V data in
InsertCell/DeleteCell markers was never read by other threads - only the
operation state matters for synchronization.

This eliminates all heap allocations from the cell operation hot path,
particularly benefiting rebalance-heavy workloads.

Benchmark improvements:
- Random inserts: +63% to +281% throughput
- Point lookups: +5% to +9% at larger dataset sizes
- Mixed workloads: +8% to +14%
- Sequential inserts: +2% to +32%
Relax SeqCst atomic orderings to Acquire on read paths for improved
performance, especially on ARM/AArch64 where SeqCst requires full
memory barriers.

Changes:
- CellGuard::from_raw(): version, marker_state, move_dest loads → Acquire
- CellGuard::cache(): version load → Acquire
- btree_map rebalance: source/dest cell reads → Acquire

The seqlock validation re-read in from_raw() remains SeqCst to prevent
reordering of data reads past the validation check on weakly-ordered
architectures.

Acquire ordering is sufficient because we only need pairwise
synchronization between a writer and its readers for each cell, not
total ordering across unrelated cells.

Added 6 tests to verify Acquire semantics work correctly.
Replace expensive key.clone() in CellGuard::from_raw() with a simple
is_some() check. The actual key/value cloning is now deferred to
cache() when the data is actually needed.

This optimization reduces overhead in the hot iteration path where code
frequently checks is_empty() to find empty slots or skip cells without
needing their actual contents. The seqlock validation still protects
against torn reads since the is_some() check happens within the
version-validated window.

Before: clone key on every from_raw() call (~10% overhead)
After:  check is_some() in from_raw(), clone only in cache()

Added tests verifying deferred cloning behavior.
Change version.store() and marker_state.store() calls after UnsafeCell
writes from SeqCst to Release ordering in the insert paths of btree_map.rs.

Release ordering is sufficient because it ensures all preceding writes
(including UnsafeCell writes to key/value) are visible to any thread that
subsequently performs an Acquire load on these atomics. This is the
"publication" side of the Release/Acquire pattern - we don't need total
ordering with writes to other cells, only pairwise synchronization with
readers of this cell.

Changes:
- 6 stores relaxed: lines ~140, ~144, ~174, ~178, ~223, ~227
- Added 5 tests verifying Release/Acquire synchronization semantics

All 116 tests pass.

Step 2 of atomic ordering relaxation plan.
Change compare_exchange and compare_exchange_weak orderings from
SeqCst/SeqCst to AcqRel/Acquire for marker_state CAS operations.

Changes:
- cell.rs: update_marker_state() CAS now uses AcqRel/Acquire
- btree_map.rs: Move marker CAS uses AcqRel/Acquire
- btree_map.rs: Best-effort cleanup CAS (version + marker) uses AcqRel/Acquire

Rationale:
- Success (AcqRel): Acquire synchronizes with prior writer's Release,
  Release publishes our claim to subsequent readers
- Failure (Acquire): Observes the blocking value; no write on failure
  so Release adds nothing

Added 5 tests verifying AcqRel/Acquire CAS semantics:
- test_cas_acqrel_synchronizes_with_prior_writes
- test_cas_failure_observes_blocking_value
- test_rebalance_cas_publishes_move_marker
- test_best_effort_cleanup_cas_correctness
- test_cas_release_sequence_with_move_dest
Step 4 of atomic ordering relaxation plan: Change move_dest.store()
from SeqCst to Release in both btree_map.rs and cell.rs.

Release ordering is sufficient because move_dest is stored before
the CAS that sets marker_state to Move. The CAS uses AcqRel ordering,
creating a release sequence that readers synchronize with via Acquire
on marker_state.

Changes:
- btree_map.rs: move_dest.store() in rebalance loop
- cell.rs: move_dest.store() in update_marker_state()

Tests added:
- test_move_dest_release_visible_after_marker_acquire
- test_move_dest_release_prevents_reorder_after_cas
- test_update_marker_state_move_dest_release
- test_concurrent_readers_observe_move_dest
Change destination cell version.store() and marker_state.store() from
SeqCst to Release ordering in rebalance (lines ~502-509). Release
ensures preceding UnsafeCell writes (key/value) are visible to readers
who Acquire-load these atomics.
…ookup=53.0, rand_lookup=50.6, mixed=117.8)

Result: {"status":"keep","total_ms":438.13,"seq_insert_ms":112.38,"rand_insert_ms":105.93,"seq_lookup_ms":52.98,"rand_lookup_ms":50.65,"mixed_ops_ms":117.84}
…t only when Move state. Saves atomic load per cell iteration.

Result: {"status":"keep","total_ms":429.6,"seq_insert_ms":110.15,"rand_insert_ms":101.09,"seq_lookup_ms":51.45,"rand_lookup_ms":50.39,"mixed_ops_ms":115.39}
…Error/CellWriteError directly. -7.2% (429.60→398.74). Lookups improved ~19%.

Result: {"status":"keep","total_ms":398.74,"mixed_ops_ms":110.25,"rand_insert_ms":98.88,"rand_lookup_ms":41.2,"seq_insert_ms":105.56,"seq_lookup_ms":42.85}
… eliminates double-clone and OnceCell construction cost. -5.8% (398.74→375.72).

Result: {"status":"keep","total_ms":375.72,"mixed_ops_ms":103.48,"rand_insert_ms":92.86,"rand_lookup_ms":38.63,"seq_insert_ms":101.73,"seq_lookup_ms":38.32}
…loning key+value for non-matching cells. -1.8% (375.72→368.80).

Result: {"status":"keep","total_ms":368.8,"mixed_ops_ms":101.62,"rand_insert_ms":91.01,"rand_lookup_ms":38.13,"seq_insert_ms":98.57,"seq_lookup_ms":39.47}
…. Avoids cloning key+value for every non-empty cell during insert scan. -0.7% (368.80→366.30).

Result: {"status":"keep","total_ms":366.3,"mixed_ops_ms":102.08,"rand_insert_ms":90.21,"rand_lookup_ms":37.81,"seq_insert_ms":97.86,"seq_lookup_ms":38.34}
…oids GCD computation in rebalance hot loop. -4.4% (366.30→350.30). rand_insert -8.4%.

Result: {"status":"keep","total_ms":350.3,"mixed_ops_ms":97.3,"rand_insert_ms":82.62,"rand_lookup_ms":37.31,"seq_insert_ms":96.08,"seq_lookup_ms":37}
…t for u64 keys (350.30→351.92). Keep for correctness/style.

Result: {"status":"keep","total_ms":351.92,"mixed_ops_ms":97.55,"rand_insert_ms":83.46,"rand_lookup_ms":37.9,"seq_insert_ms":94.1,"seq_lookup_ms":38.91}
…d to compare_key/clone_value/cache. -22.2% (351.92→273.61). Lookups -40%!

Result: {"status":"keep","total_ms":273.61,"mixed_ops_ms":80.27,"rand_insert_ms":70.42,"rand_lookup_ms":22.46,"seq_insert_ms":77.89,"seq_lookup_ms":22.58}
…ad per empty cell iteration. -3.6% (273.61→263.88). Inserts -5%.

Result: {"status":"keep","total_ms":263.88,"mixed_ops_ms":75.04,"rand_insert_ms":67.32,"rand_lookup_ms":23.52,"seq_insert_ms":74.31,"seq_lookup_ms":23.7}
…er empty cell. -0.9% (263.88→261.48).

Result: {"status":"keep","total_ms":261.48,"mixed_ops_ms":76.04,"rand_insert_ms":67.29,"rand_lookup_ms":22.49,"seq_insert_ms":72.85,"seq_lookup_ms":22.25}
…matching cells. Only creates CellGuard for Move handling. -1.8% (261.48→256.75). Lookups -8.6%.

Result: {"status":"keep","total_ms":256.75,"mixed_ops_ms":74.84,"rand_insert_ms":66.1,"rand_lookup_ms":20.57,"seq_insert_ms":75.22,"seq_lookup_ms":20.02}
…s in insert. Avoids heap allocation per insert. -8.2% (256.75→235.65). rand_insert -39.5%!

Result: {"status":"keep","total_ms":235.65,"mixed_ops_ms":76.71,"rand_insert_ms":40,"rand_lookup_ms":23.03,"seq_insert_ms":73.26,"seq_lookup_ms":22.65}
…-lock avoids unnecessary write lock acquisitions. -3.4% (235.65→227.73).

Result: {"status":"keep","total_ms":227.73,"mixed_ops_ms":75.36,"rand_insert_ms":38.99,"rand_lookup_ms":20.71,"seq_insert_ms":72.62,"seq_lookup_ms":20.06}
…find/node_at/left_child/right_child, CellGuard is_empty/marker_state/move_dest, within_density_threshold. -38.4% (227.73→140.25)!

Result: {"status":"keep","total_ms":140.25,"mixed_ops_ms":47.18,"rand_insert_ms":26.66,"rand_lookup_ms":10.32,"seq_insert_ms":44.65,"seq_lookup_ms":11.43}
… improvement -1.2% (140.25→138.61).

Result: {"status":"keep","total_ms":138.61,"mixed_ops_ms":43.59,"rand_insert_ms":25.45,"rand_lookup_ms":10.28,"seq_insert_ms":41.87,"seq_lookup_ms":10.35}
…ovement -0.6% (138.61→137.72).

Result: {"status":"keep","total_ms":137.72,"mixed_ops_ms":43.02,"rand_insert_ms":25.3,"rand_lookup_ms":10.48,"seq_insert_ms":41.11,"seq_lookup_ms":10.58}
…lidation deferred to compare_key/clone_value. -3.1% (137.72→133.47).

Result: {"status":"keep","total_ms":133.47,"mixed_ops_ms":42.9,"rand_insert_ms":24.84,"rand_lookup_ms":9.92,"seq_insert_ms":42.59,"seq_lookup_ms":10.11}
…egligible perf impact (133.47→135.66).

Result: {"status":"keep","total_ms":135.66,"mixed_ops_ms":42.91,"rand_insert_ms":23.97,"rand_lookup_ms":10.72,"seq_insert_ms":42.82,"seq_lookup_ms":10.29}
jahfer added 16 commits March 20, 2026 10:20
… cells, only create CellGuard for actual insertion/rebalance. -11.9% (135.66→119.58). Inserts -22%, mixed -14%.

Result: {"status":"keep","total_ms":119.58,"mixed_ops_ms":37.94,"rand_insert_ms":18.2,"rand_lookup_ms":10.05,"seq_insert_ms":35.94,"seq_lookup_ms":10.87}
… check. Marginal improvement within noise (119.58→117.98 on best run).

Result: {"status":"keep","total_ms":117.98,"mixed_ops_ms":39.23,"rand_insert_ms":19.32,"rand_lookup_ms":11.63,"seq_insert_ms":36.76,"seq_lookup_ms":11.05}
…lls skip the Acquire atomic load entirely. -3.6% (117.98→113.73).

Result: {"status":"keep","total_ms":113.73,"mixed_ops_ms":38.01,"rand_insert_ms":18.45,"rand_lookup_ms":10.01,"seq_insert_ms":36.85,"seq_lookup_ms":10.1}
… perf change (113.73→116.85 noise).

Result: {"status":"keep","total_ms":116.85,"mixed_ops_ms":38.95,"rand_insert_ms":18.96,"rand_lookup_ms":10.69,"seq_insert_ms":37.63,"seq_lookup_ms":10.63}
… lock case. Marginal -1.0% (116.85→115.79 avg).

Result: {"status":"keep","total_ms":115.79,"mixed_ops_ms":37.88,"rand_insert_ms":17.75,"rand_lookup_ms":9.82,"seq_insert_ms":36.77,"seq_lookup_ms":10.25}
…ence. No measurable improvement (115.79→115.72). Keep for cleaner access.

Result: {"status":"keep","total_ms":115.72,"mixed_ops_ms":39.75,"rand_insert_ms":18.16,"rand_lookup_ms":9.18,"seq_insert_ms":38.15,"seq_lookup_ms":10.5}
…), iterate in reverse for spreading. Vec avoids ring buffer overhead. -3.5% (116.23→112.22).

Result: {"status":"keep","total_ms":112.22,"mixed_ops_ms":37.67,"rand_insert_ms":17.39,"rand_lookup_ms":9.52,"seq_insert_ms":36.87,"seq_lookup_ms":9.56}
…Avoids reallocation during density scan. -2.0% (112.22→109.96).

Result: {"status":"keep","total_ms":109.96,"mixed_ops_ms":35.65,"rand_insert_ms":17.24,"rand_lookup_ms":9.46,"seq_insert_ms":35.38,"seq_lookup_ms":9.47}
…hin_density_threshold. Eliminates O(8) linear scan per density check. -2.3% (109.96→107.43).

Result: {"status":"keep","total_ms":107.43,"mixed_ops_ms":35.39,"rand_insert_ms":16.75,"rand_lookup_ms":9.22,"seq_insert_ms":34.09,"seq_lookup_ms":9.59}
…ad code path, lets compiler generate tighter match. -1.4% (107.43→106.31 avg).

Result: {"status":"keep","total_ms":106.31,"mixed_ops_ms":34.59,"rand_insert_ms":17.88,"rand_lookup_ms":9.32,"seq_insert_ms":33.73,"seq_lookup_ms":9.92}
…thin noise (106.31→106.47).

Result: {"status":"keep","total_ms":106.47,"mixed_ops_ms":34.66,"rand_insert_ms":17.57,"rand_lookup_ms":10.11,"seq_insert_ms":34.22,"seq_lookup_ms":9.91}
…ersion validation. Lookups -16%. -2.1% total (106.47→104.26 best, ~105 avg).

Result: {"status":"keep","total_ms":104.26,"mixed_ops_ms":35.46,"rand_insert_ms":16.99,"rand_lookup_ms":8.29,"seq_insert_ms":33.79,"seq_lookup_ms":8.41}
…ition for find(). -4.5% (103.15→98.53). rand_insert -8.8%, mixed -5.6%.

Result: {"status":"keep","total_ms":98.53,"mixed_ops_ms":33.15,"rand_insert_ms":15.6,"rand_lookup_ms":8.2,"seq_insert_ms":33.46,"seq_lookup_ms":7.99}
… read paths. Within noise (98.53→98.76).

Result: {"status":"keep","total_ms":98.76,"mixed_ops_ms":33.63,"rand_insert_ms":15.61,"rand_lookup_ms":9.18,"seq_insert_ms":31.3,"seq_lookup_ms":9.04}
…ta dependency). Marginal -1.2% (98.76→96.10 best).

Result: {"status":"keep","total_ms":96.1,"mixed_ops_ms":32.15,"rand_insert_ms":15.38,"rand_lookup_ms":8.08,"seq_insert_ms":31.21,"seq_lookup_ms":8.09}
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