Baseline measurements for miniextendr’s runtime overhead on Apple M3 Max (macOS, Rust 1.93, R 4.5). Run date: 2026-02-18.

🔗Quick Reference

SubsystemOperationMedianNotes
Worker thread (requires worker-thread feature)round-trip5 usrun_on_worker channel hop
Worker thread (requires worker-thread feature)with_r_thread (main)14 nsalready on main thread
Unwind protectwith_r_unwind_protect32 nsoverhead vs direct call
Unwind protectnested 5 layers169 nslinear scaling
catch_unwindsuccess path0.5 nsno panic
catch_unwindpanic caught6.3 uspanic + catch overhead
ExternalPtrcreate (8 B)83 nsvs Box 42 ns (2x)
ExternalPtrcreate (64 KB)168 nsvs Box 1.1 us
Trait ABIvtable query~1 nscache-hot, 2 or 5 methods
Trait ABIdispatch (1 method)55-63 nsfull view dispatch
Trait ABIdispatch (all 5)417 nsmulti-method hot loop
R allocatorsmall (8 B)61 nsvs system 17 ns (3.6x)
R allocatorlarge (64 KB)1.2 usvs system 500 ns (2.4x)

🔗Type Conversions

🔗Rust to R (into_sexp)

TypeSizeMedianNotes
i32112 nsscalar
i321K378 nsmemcpy
i321M105 us
f641M220 us
String1M~60 msCHARSXP allocation dominates
Option<i32> 50% NA1M391 us
i64 (smart)scalar40-43 nsINTSXP or REALSXP

🔗R to Rust (try_from_sexp)

TypeSizeMedianNotes
i32 scalar130 ns
f64 scalar127 ns
f64 sliceany~21 nszero-copy (pointer cast)
i32 sliceany~21 nszero-copy (pointer cast)
String138 nsUTF-8 (no re-encode needed)
String (Latin1)1250 nsrequires re-encoding
Vec<i32> → HashSet64K1.5 mshashing overhead

🔗Strict Mode

Negligible overhead for scalar conversions (~2-5 ns). Vec<i64> at 10K: strict 6.2 us vs normal 12.4 us (strict is actually faster due to INTSXP-only fast path avoiding REALSXP conversion).

🔗Coercion

OperationMedianNotes
scalar int direct23 nsno coercion
scalar int→real (R)31 nsRf_coerceVector
scalar int→real (Rust)23 nsRust-side cast
vec int→real (256 elts, R)350 nsR as.double()
vec int→real (256 elts, Rust)265 nsRust-side conversion

Rust-side coercion is ~25% faster than R’s Rf_coerceVector for vectors.

🔗DataFrames

OperationRowsMedianNotes
Point3 → SEXP100750 ns3 f64 columns
Point3 → SEXP100K273 us
Event (enum) → SEXP100K7.1 ms5 columns, string-heavy
Mixed → SEXP100K10.5 ms7 columns, mixed types
Transpose (Point3)100K246 usrow→column pivot
Transpose (wide 10-col)100K1.4 ms

🔗ALTREP

OperationSizeALTREPPlainRatio
element access (elt)1220 ns9 ns24x
DATAPTR materialization64K17-20 us9 ns
full scan (elt loop)64K5.2 ms2.7 us~1900x
full scan (DATAPTR)64K20 us9 ns

🔗Guard Modes (64K elements, full scan)

GuardMedian
unsafe16.7 ms
rust_unwind (default)17.5 ms
r_unwind21 ms
plain INTSXP261 us

unsafe and rust_unwind are equivalent. r_unwind adds ~25% overhead due to R_UnwindProtect per callback.

🔗String ALTREP (64K strings)

OperationMedian
create2.6 ms
elt access2.7 ms
elt with NA2.4 ms
force materialize (DATAPTR_RO)6.9 ms
plain STRSXP elt4.7 ms

🔗Zero-Allocation (constant real)

OperationSizeMedian
create constantany229 ns
constant eltany513 ns
constant full scan64K17.9 ms
vec-backed full scan64K5.2 ms

🔗Connections

OperationSizeMedian
build + open583 ns
write128 B25 ns
read64 B24 ns
read16 KB1.7 us
write16 KB1.0 us
burst write (50x 256 B)12.8 KB total1.2 us

🔗R Wrapper Dispatch

Class SystemMedianNotes
plain fn call125 nsbaseline
env $ dispatch166 nsnative env lookup
R6 $ dispatch364 ns
S3 UseMethod521 ns
S4 setMethod542 ns
S7 dispatch2.8 us
wrapper overhead229 nswrapper fn → inner fn
as.integer() coercion291 nsscalar
as.character() coercion625 nsscalar

🔗GC Protection

See analysis/gc-protection-strategies.md for full analysis and analysis/gc-protection-benchmarks-results.md for detailed results. All numbers below measure pure protection cost (SEXP allocation excluded).

🔗Per-operation cost

MechanismSingle opNotes
Protect stack7.4 nsarray write + integer subtract
Vec pool (VECSXP)9.6 nsSET_VECTOR_ELT + free list
Slotmap pool11.4 ns+ generational safety check
Precious list13.1 nsCONS alloc + prepend
DLL preserve28.9 nsCONS alloc + doubly-linked splice

🔗Batch throughput (protect N, release all)

Mechanism1k10k50k
Protect stack3.8 µs38 µs— (50k limit)
Vec pool9.6 µs97 µs486 µs
Slotmap pool11.7 µs116 µs575 µs
DLL preserve27.2 µs256 µs1.31 ms
Precious list568 µs

🔗Replace-in-loop (N replacements)

Mechanism10kPer-op
ReprotectSlot37.6 µs3.8 ns
Pool overwrite45.2 µs4.5 ns
DLL reinsert271 µs27.1 ns
Precious churn15.1 s1.5 ms (O(n²))

🔗Typed List Validation

FieldsMedian
3682 ns
102.1 us
5012.8 us

Linear scaling (~240 ns/field).

🔗Factors

OperationMedian
single (cached)58 ns
single (uncached)372 ns
100 repeated (cached)5.5 us
Vec<Factor> (4096)4.4 us

🔗Lint (miniextendr-lint)

BenchmarkScaleMedian
full_scan10 modules1.9 ms
full_scan100 modules16.3 ms
full_scan500 modules84.9 ms
impl_scan10 types1.9 ms
impl_scan100 types16.8 ms
scaling500 fns, 10 files5.9 ms
scaling500 fns, 500 files67.9 ms

Linear scaling in both module count and file count.

🔗FFI Call Overhead

OperationSizeMedian
Rf_ScalarInteger11 ns
Rf_ScalarReal12 ns
Rf_ScalarLogical4 ns
INTEGER_ELTany7.5 ns
REAL_ELTany7.6 ns
Rf_protect/unprotect118 ns
Rf_allocVector (INTSXP, 64K)64K~235 ns

🔗Reproducing

# Full Rust suite
cargo bench --manifest-path=miniextendr-bench/Cargo.toml

# Connections (feature-gated)
cargo bench --manifest-path=miniextendr-bench/Cargo.toml --features connections --bench connections

# Lint benchmarks
cargo bench --manifest-path=miniextendr-lint/Cargo.toml --bench lint_scan

# Save structured baseline
just bench-save

Raw results: miniextendr-bench/BENCH_RESULTS_2026-02-18.md