Skip to content

feat(ffi): measure the bindings on the binding benchmark, and commit the numbers - #695

Draft
kixelated wants to merge 4 commits into
mainfrom
quest/m1/ffi-export/bench
Draft

kixelated wants to merge 4 commits into
mainfrom
quest/m1/ffi-export/bench

Conversation

@kixelated

@kixelated kixelated commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

Finishes quest/m1/ffi-export/bench.md.

Problem

The repo had no committed measurement of rust2-ffi's bindings. just ffi bench printed ad hoc timings and saved nothing, so the epic's rule that every generator "is measured, with its generated size" had no baseline. The speed work, along with checks-raise and swift-noncopyable, had nothing to measure against.

Approach

  • ffi/moq/bench/export.{c,cpp,kt,py,swift,go,dart} now time one set of cases, mapped onto BoltFFI's table. A new export.rs times the same cases as the Rust floor, on the stand-in's API. The cases: handle call, u64 getter, record return, object return, frame write (plain and stamped), ready read, woken read, payload, and write-plus-read throughput at 100 B to 1 MiB. Each case reports wall time and process CPU time.
  • just ffi bench = bench-build + bench-run [rounds]. bench-run records the 1-minute load average over each program run into target/bench/raw.tsv. Then bench/ffi/report.py prints the median of each case beside its median load, plus the sizes of the generated bindings, their Rust glue, and the native libraries (raw, gzip, and stripped). The nightly job's summary gets the same tables.
  • bench/ffi/README.md commits the numbers and the analysis: which costs a call spends, ranked by what they cost moq's frame path; a per-handle baseline for checks-raise and swift-noncopyable; and a comparison with the BoltFFI targets. The quest file is deleted, and the links to it now point at the README.

Loaded host: at the maintainer's direction this was measured through plain quest gate, not --host. The 1-minute load average stayed between 22 and 29 on 24 threads for the whole run. Each cell shows its load and is the median of 5 rounds, and the README marks the numbers as taken on a loaded host. Use the CPU tables first. Large-frame throughput (16 KiB and up) is bandwidth-bound and moved 2x between runs, so the README tells readers to use only the 100 B and 1 KiB rows.

Headline numbers (CPU ns, median):

Rust C Swift Kotlin (wall) Python Go Dart
u64 getter (per-handle baseline) 0.4 10 18 24 47 41 29
object return 1.1 25 50 137 97 263 242
new Timestamp handle per frame 0 21 27 92 140 256 245

Choices made without asking:

  • Process CPU time, not instruction counts. Instruction counts per case would need perf_event_open in each of 8 languages. CPU time counts every thread, so it includes the JVM's JIT/GC threads and Swift's executor workers (documented).
  • The CPU-per-MB table was replaced by MB per CPU second, the same measure inverted.
  • Per-language native glue deltas (a separate release build per feature) were dropped: three extra full builds on a saturated host. The report gives each binding's generated Rust glue source size instead.
  • A woken read starts in each language's own way (Task, goroutine, ensure_future, an undispatched coroutine). It therefore includes starting a task, as the README says.
  • just ffi bench depends on moq-export now, not on the whole just ffi moq test run.

Impact

  • Public API: none. Wire: none.
  • just ffi bench gains bench-build and bench-run [rounds], and writes ffi/target/bench/report.md.
  • The nightly ffi-bench job is renamed "Bench the bindings".

Alternatives

  • Measuring under quest gate --host (dropped at the maintainer's direction; it starved other agents).
  • Per-case perf stat instruction counts: more stable under load, but each language would need its own per-case counter bindings. Not worth the cost while CPU time already separates the costs by 2x or more.

Follow-ups

Recommended to plan with /quest-plan, from the ranking in the README:

  1. Plan: Copy types cross by value. Timestamp is Copy in Rust but exports as a handle, which costs 92 to 256 ns per frame in Kotlin, Python, Go, and Dart (this doubles the write in Go and Dart). Export a Copy, Drop-free type with private fields as an opaque value copied across, not an Arc handle.
  2. Plan: cheaper returned-object ownership in Go and Dart. At 263 and 242 ns, ten times C's 25, from runtime.AddCleanup and NativeFinalizer registration on every frame, group, and future returned.
  3. Plan: Kotlin records without the byte-array codec. 113 ns against C's 31 and Swift's 37. This confirms the audit's item.
  4. Plan, measuring first: the woken read in Swift and Go. 5 to 6 µs wall, and Swift burns 16.6 µs of CPU. Add a long-lived reader-loop case before changing a generator, because this case starts a task per read.
  5. Drop: a per-handle poll in the C ABI. A ready read costs C 155 ns over the write where Rust costs 73. Each language adds 160 to 420 ns in its own wrapper on top of that, so the C ABI's future is not the bottleneck yet.
  6. Drop: keeping Python's GIL during calls. Python's getter (47 ns) already beats BoltFFI's (63 ns), which keeps the GIL.

Already planned: checks-raise and swift-noncopyable. The README's per-handle baseline section gives them the u64-getter and handle-call rows to measure against.

Decisions

Both decisions were taken unattended on 2026-11-10 by the coordinating agent while the maintainer was away. They follow this PR's recommendations; confirm them when you merge.

  • ✅ No quest files in this PR: /quest-plan plans follow-ups 1 to 4 when the PR merges, and 5 and 6 stay dropped. Rejected: creating the quests here, as the quest's Goal wording suggested, which the quest guide reserves for /quest-plan.
  • ✅ Land on the loaded-host numbers, without re-measuring on a quiet host. Every cost in the ranking is separated by 2x or more, and the nightly job re-measures on a dedicated arm64 runner. Rejected: holding the PR for a quiet-host run.

Models

Size: M
Implementer: Claude Code, Opus 5.5, medium effort

🤖 Generated with Claude Code

(Written by Opus 5.5 with medium)

Review

Rewritten by quest review after each round.

  • Reviewer: Codex, gpt-6.1-sol, medium effort
  • Session: 01a1c761-dbe6-7fc3-ab54-dbedce961a18, round 1
  • Head: 8afc78e, named in full once a round reports no findings
  • Base: origin/main at efbfa66
  • Score: 7/10, The expanded benchmark needs two macOS portability fixes before the supported Darwin workflow can produce a report.
  • Findings, open until the next round settles them:
    • Medium, ffi/justfile:100: Read load averages through a platform-aware interface
    • Medium, bench/ffi/report.py:57: Select native library filenames for the host platform

@pullfrog

pullfrog Bot commented Nov 10, 2026

Copy link
Copy Markdown

Pullfrog  | Review this ➔

kixelated and others added 3 commits November 10, 2026 11:16
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…enchmark

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pends

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title The exporter on the binding benchmark feat(ffi): measure the bindings on the binding benchmark, and commit the numbers Nov 10, 2026
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