Skip to content

perftest: twin-handle lifecycle benchmark — plain vs .gc_managed()#83

Merged
milyin merged 1 commit into
mainfrom
perftest-gc-managed
Jul 17, 2026
Merged

perftest: twin-handle lifecycle benchmark — plain vs .gc_managed()#83
milyin merged 1 commit into
mainfrom
perftest-gc-managed

Conversation

@milyin

@milyin milyin commented Jul 17, 2026

Copy link
Copy Markdown
Owner

What

Measures the real cost of the .gc_managed() machinery (#82) with a twin minimal handle pair — Token (plain) vs TokenGc (.gc_managed()), identical Rust shape — plus six new perftest rows.

Results (medians of 3 runs, N=5M classic / 2M handle rows, Apple Silicon)

row plain gc delta
handle_close — create + explicit close 34 ns 119 ns +85 ns
handle_drop — create + drop, no close 20 ns* 171 ns +151 ns
handle_call — method on a live handle 18.9 ns 18.9 ns 0

* plain drop leaks natively (allocation cost only — today's leak-on-drop behavior); the gc number includes the Cleaner-driven free and PhantomRef/GC churn.

Existing hot paths are unaffected: pre-#82 (598f740) vs this branch, medians of the untouched put/get/callback(+_vec) rows differ only within noise (±3%, mixed signs) — the NativeHandle.ptr open-property change costs nothing for plain classes, and the atomic-cell getter matches the plain field on the call path.

Interpretation for the zenoh hot path

A received zenoh sample materializes 2 handles (KeyExpr + payload ZBytes). GC-managing them would add roughly 85–150 ns × 2 ≈ 0.2–0.3 µs per message of allocation/registration/cleanup work against the ~1 µs/msg subscriber floor — i.e. a potential double-digit percentage throughput cost, to be confirmed end-to-end with run_thr_java.sh (follow-up measurement). The per-class opt-out for KeyExpr/ZBytes in zenoh-flat-jni stays justified; the zero call-path cost confirms the backstop classes (Session, Publisher, …) pay only at create/close time.

265 lib tests green; fmt + clippy clean; covertest untouched.

🤖 Generated with Claude Code

Add a minimal opaque twin pair (Token / TokenGc, identical Rust shape,
one plain, one .gc_managed()) and six Bench.kt rows pricing the GC-
cleanup machinery head-to-head per handle:

  handle_close  create + explicit close   plain  34 ns   gc 119 ns
  handle_drop   create + drop (no close)  plain  20 ns*  gc 171 ns
  handle_call   method on a live handle   plain  19 ns   gc  19 ns
  (* plain drop leaks natively — allocation cost only, today's
     leak-on-drop behavior; gc includes the Cleaner-driven free)
  (medians of 3 runs, N=5M classic rows / 2M handle rows, Apple Silicon)

So the mechanism costs ~85 ns extra on an explicitly closed handle and
~150 ns on a GC-collected one (cell alloc + Cleaner registration + CAS
ticket + PhantomRef churn), and NOTHING on the call path — the atomic-
cell ptr getter matches the plain field, and pre-#82 vs post-#82 medians
of the untouched put/get/callback rows differ only within noise (±3%,
mixed signs), so the NativeHandle.ptr open-property change is free for
plain classes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@milyin

milyin commented Jul 17, 2026

Copy link
Copy Markdown
Owner Author

Follow-up: end-to-end zenoh throughput measurement

run_thr_java.sh (CROSS=0, 3 passes × 8 rounds × 100k msgs, loopback peer-peer), flat×flat subscriber medians, with KeyExpr + ZBytes temporarily flipped to .gc_managed() in zenoh-flat-jni (the two per-message handle classes — one of each is materialized per received sample; the payload handle is explicitly closed after its lazy read, the keyexpr handle is dropped and Cleaner-freed):

payload baseline (plain) gc_managed delta per-msg cost
8 B 891,880 msg/s 685,624 msg/s −23.1% +337 ns
1 KB 652,189 msg/s 562,413 msg/s −13.8% +245 ns

(main×main control column stayed at ~490k/~430k msg/s across both runs — machine stable.)

The observed +245–337 ns/msg matches the microbench prediction (~85 ns close-path + ~151 ns drop-path ≈ 236 ns, plus GC-pressure amplification from the PhantomRef churn at ~700k registrations/s). Conclusion: the per-class opt-out for hot-path per-message handles (KeyExpr, ZBytes) is justified — gc-managing them costs a double-digit throughput percentage; the backstop classes (Session, Publisher, …, Encoding) pay only at create/close time, and nothing on the call path. The temporary flip was reverted; zenoh-flat-jni is unchanged.

🤖 Generated with Claude Code

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