Skip to content

perf: fast native RangeSet mutation with selectable locking - #20

Merged
josephjohncox merged 1 commit into
mainfrom
perf/native-result-construction
Jul 22, 2026
Merged

perf: fast native RangeSet mutation with selectable locking#20
josephjohncox merged 1 commit into
mainfrom
perf/native-result-construction

Conversation

@josephjohncox

Copy link
Copy Markdown
Owner

Summary

Makes the cpp_boundary-backed RangeSet mutation hot path much faster and adds a fully-native scalar mutation surface with a user-selectable locking level. Four compounding changes, all preserving the exact public MutationResult/Span/IntervalResult frozen-dataclass contracts and every existing invariant (half-open signed-int geometry, failure atomicity, reentrancy protection, deterministic ordering, and snapshot/mutation consistency in synchronized mode).

Baseline add/discard was 0.92 M ops/s. The new fully-native scalar unsynchronized path reaches ~5.06 M ops/s — about 5.5× the default and within ~65% of the raw-native floor.

Changes

  1. Trusted validation-free native result builders (domain._native_span / _native_mutation_result). The native manager only emits int64 geometry it has already validated, so re-running Span.__post_init__ on every returned delta was pure overhead. The builders construct the same frozen-dataclass instances (equal value, hash, ordering, repr, pickle) without that redundant validation. Public Span(...) construction still validates.
  2. Hot-path collapse for add/discard. The authoritative path caches the bound native release_with_delta/reserve_with_delta at construction — behind a fail-closed contract probe — and skips the BackendAdapter wrapper frame plus the per-op isinstance check.
  3. User-selectable locking (synchronized). RangeSet(..., synchronized=True) (default), create_range_set(..., synchronized=True), and BackendRegistry.create(..., synchronized=True) keep the reentrant lock and full thread-safety. synchronized=False skips the per-op with self._lock: protocol on the hot path entirely (its __enter__/__exit__ calls, not lock contention, were the cost); the caller then owns all cross-thread synchronization and snapshot/mutation consistency. Both hot and non-hot paths use the same lock object, so synchronized mutual exclusion is intact.
  4. Fully-native scalar surface. RangeSet.release(span) -> int (free/add semantics) and RangeSet.reserve(span, *, require_covered=False) -> int (occupy/discard semantics) return only the changed length, build no MutationResult, and are exactly geometry-consistent with add/discard. They require an authoritative geometry-only range set and raise a clear ValueError otherwise — never a silent fallback. On cpp_boundary they use dedicated native scalar mutators (release_delta_length/reserve_delta_length); other authoritative backends derive the exact changed length from the delta.

Measured (cpp_boundary, Apple M5 Max, macOS 26.5.1, CPython 3.12.7, 30-sample hot-path benchmark)

Path Median throughput vs native floor
add/discard (MutationResult), synchronized ~1.54 M ops/s
add/discard (MutationResult), unsynchronized ~1.65 M ops/s
scalar release/reserve, synchronized ~4.32 M ops/s ~55%
scalar release/reserve, unsynchronized ~5.06 M ops/s ~65%
plain-native floor (raw IntervalManager) ~7.9 M ops/s 100%

Two levers compound: dropping eager MutationResult/Span construction is the larger win (~3× over add/discard); synchronized=False then removes the per-op lock protocol (~4.3M → ~5.06M). Scoped evidence for one interface family, one restorative workload, one host, one timing layer — not a universal claim. Reaching the literal 7.9M floor would require dropping the residual RangeSet bookkeeping (reentrancy guard, _total_free, cache invalidation), which would sacrifice the invariants above.

New public API

  • RangeSet(..., synchronized=True), RangeSet.synchronized
  • RangeSet.release(span) -> int, RangeSet.reserve(span, *, require_covered=False) -> int
  • create_range_set(..., synchronized=True), BackendRegistry.create(..., synchronized=True)
  • BackendAdapter.supports_scalar_delta, release_delta_length, reserve_delta_length
  • RangeSetProtocol.release, .reserve
  • native IntervalManager.release_delta_length, reserve_delta_length

Correctness & evidence

  • New tests/unit/test_rangeset_scalar_hotpath.py: scalar vs MutationResult differential across every authoritative stable backend, boundary/negative/int64-extreme/no-op cases, both locking levels, single-threaded agreement, concurrent-consistency under synchronized mode, construction-probe fail-closed, capability guards, atomicity on domain/overflow and injected result-construction failure, and exact frozen-dataclass identity (trusted-built spans equal validated ones; public construction still validates).
  • Updated test_native_core_contract.py atomicity injection to the real trusted-builder allocation point (domain._object_new), proving result materialization precedes the native mutation.
  • New tests/performance/rangeset_hotpath_benchmark.py + scripts/verify_rangeset_hotpath_benchmark.py + test_rangeset_hotpath_benchmark_artifact.py: canonical JSON/Markdown/SHA-256 triplet with strict duplicate-key/non-finite/exact-type rejection and source/runtime/binary provenance; same-instance correctness validated outside timing.
  • docs/performance.md: "Locking levels and the fully-native scalar path" section with the measured table and the explicit unsynchronized concurrency contract; Justfile benchmark-hotpath recipe.

Validation

  • Full suite: 1206 passed, 16 skipped (no regressions).
  • mypy treemendous clean (114 files); Ruff check + format clean; git diff --check clean.
  • Native extension rebuilt; trusted-builder equality/hash/pickle-shape preserved.

Boundaries preserved

  • No change to add/discard/MutationResult/Span/IntervalResult public semantics or types.
  • No silent fallback: scalar mutators fail closed on payload policies and non-authoritative/non-scalar backends; the authoritative probe fails closed if a backend breaks the delta contract.
  • synchronized=False is opt-in and its concurrency contract is documented explicitly.

@josephjohncox
josephjohncox force-pushed the perf/native-result-construction branch from b317ec6 to c750778 Compare July 22, 2026 21:36
Trusted validation-free native result builders, cached bound native
mutators with a construction-time contract probe, collapsed authoritative
add/discard (no adapter frame or per-op isinstance), a user-selectable
locking level (synchronized=True default; unsynchronized skips the per-op
lock protocol), and a fully-native scalar release/reserve surface that
returns only the changed length.

cpp_boundary, Apple M5 Max, 30-sample hot-path benchmark:
  add/discard MutationResult  synchronized 1.54M/s  unsynchronized 1.65M/s
  scalar release/reserve      synchronized 4.32M/s  unsynchronized 5.06M/s
  plain-native floor          7.9M/s
The unsynchronized scalar surface is ~5.5x the default add/discard and
within ~65% of the raw-native floor, exactly geometry-consistent.
@josephjohncox
josephjohncox force-pushed the perf/native-result-construction branch from c750778 to 0995217 Compare July 22, 2026 21:37
@josephjohncox
josephjohncox merged commit d99f108 into main Jul 22, 2026
16 checks passed
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