diff --git a/ffi/moq/bench/export.py b/ffi/moq/bench/export.py index f2be2e840..18e633295 100644 --- a/ffi/moq/bench/export.py +++ b/ffi/moq/bench/export.py @@ -52,6 +52,15 @@ def round(loop): closed += 1 call = per_frame(start) + # A u64 getter, timed as bench/ffi/README.md timed BoltFFI's `group.sequence()`: a bound + # method called in a loop. BoltFFI holds the GIL through the call, where the export + # releases it so Rust may call Python back from another thread, at about 40 ns a call. + sequence = track.append_group().demand().sequence + start = time.perf_counter_ns() + for _ in range(GROUPS * FRAMES): + sequence() + getter = per_frame(start) + # A record returned: the track's info, its fields made as Python objects. start = time.perf_counter_ns() for _ in range(GROUPS * FRAMES): @@ -103,7 +112,7 @@ async def woken(): bang = "!" if closed != 0 else "" return ( f"export (Python): {stamped:.0f} ns per stamped 100 B frame, {reused:.0f} reusing the timestamp, " - f"{call:.1f} ns per handle call, {info:.0f} ns per record return, {read:.0f} ns per frame written and read, " + f"{call:.1f} ns per handle call, {getter:.1f} ns per u64 getter, {info:.0f} ns per record return, {read:.0f} ns per frame written and read, " f"{wake:.0f} ns per frame written and read after a wake, {lent:.0f} ns per payload read{bang}" ) diff --git a/ffi/runtime/python.rs b/ffi/runtime/python.rs index ce402810d..a5002fc7a 100644 --- a/ffi/runtime/python.rs +++ b/ffi/runtime/python.rs @@ -198,8 +198,9 @@ impl Key { pub struct Scope { keep: Vec, arrays: Vec>, - held: Vec, - moved: Vec<*mut Handle>, + /// The handles held, each with whether the call moves its value out. Inline, as a + /// call holds few, so holding one allocates nothing. + held: smallvec::SmallVec<[(Obj, bool); 4]>, /// The objects Rust lends a callback, ended once it returns. lent: Vec, /// Python implementations and waker nodes given to Rust, taken back unless the call @@ -219,11 +220,13 @@ impl Scope { /// Note that the call happened: what it moved out of is closed, and what it was given /// is Rust's. pub fn done(&mut self) { - for handle in self.moved.drain(..) { - // SAFETY: a handle this scope holds, so live. - unsafe { close(handle) }; + for (held, moved) in &self.held { + if *moved { + // SAFETY: a handle this scope holds, so live. + unsafe { close(held.ptr().cast()) }; + } } - for given in self.given.drain(..) { + while let Some(given) = self.given.pop() { given.into_ptr(); } self.nodes.clear(); @@ -231,21 +234,22 @@ impl Scope { /// The handles held, which a future keeps until it ends. pub fn take_held(&mut self) -> Vec { - std::mem::take(&mut self.held) + self.held.drain(..).map(|(held, _)| held).collect() } } +// Popping rather than draining, as each call drops a scope whose lists are mostly empty. impl Drop for Scope { fn drop(&mut self) { - for waker in self.nodes.drain(..) { + while let Some(waker) = self.nodes.pop() { // SAFETY: a reference the call never took. unsafe { node_drop(waker.data) }; } - for lent in self.lent.drain(..) { + while let Some(lent) = self.lent.pop() { // SAFETY: an object [`lent`] made. unsafe { end(lent.ptr().cast()) }; } - for held in self.held.drain(..) { + while let Some((held, _)) = self.held.pop() { // SAFETY: a handle this scope holds. unsafe { release(held.ptr().cast()) }; } @@ -1015,10 +1019,7 @@ pub fn hold(obj: *mut PyObject, index: usize, s: &mut Scope, moved: bool) -> R<* return value_error(&format!("{} is closed", type_name(ty(index)))); } (*handle).holds += 1; - s.held.push(Obj::borrowed(obj)); - if moved { - s.moved.push(handle); - } + s.held.push((Obj::borrowed(obj), moved)); Ok((*handle).raw) } } diff --git a/ffi/src/glue.rs b/ffi/src/glue.rs index 39cd8700d..d8548c218 100644 --- a/ffi/src/glue.rs +++ b/ffi/src/glue.rs @@ -128,6 +128,7 @@ impl Crate<'_> { bytes = \"1\"\n\ jni-sys = {{ version = \"0.4\", optional = true }}\n\ pyo3-ffi = {{ version = \"0.30\", optional = true, features = [\"abi3-py311\", \"extension-module\"] }}\n\ + smallvec = {{ version = \"1.16\", optional = true }}\n\ \n\ [build-dependencies]\n\ pyo3-build-config = {{ version = \"0.30\", optional = true }}\n\ @@ -136,7 +137,7 @@ impl Crate<'_> { # The JNI functions kotlin/{lib}.kt calls.\n\ kotlin = [\"dep:jni-sys\"]\n\ # The CPython extension python/{lib}.py loads.\n\ - python = [\"dep:pyo3-ffi\", \"dep:pyo3-build-config\"]\n\ + python = [\"dep:pyo3-ffi\", \"dep:pyo3-build-config\", \"dep:smallvec\"]\n\ # What dart/{lib}.dart calls that `dart:ffi` lacks.\n\ dart = []\n\ \n\ diff --git a/quest/m3/README.md b/quest/m3/README.md index 405f20971..f94ac2feb 100644 --- a/quest/m3/README.md +++ b/quest/m3/README.md @@ -33,6 +33,5 @@ generic std gap is planned here even when moq never calls it. - [A vector's try_reserve](/quest/m3/ts-vec-rest.md) - `try_reserve` and `try_reserve_exact` of a `Vec` and a `VecDeque` translate - [Fresh writes beside a zipped iterator that points at a part](/quest/m3/ts-fresh-shapes.md) - zipping slots with a `Vec<[T; N]>`, `vec![row; n]`, or `copied` of what the slot holds translates - [Python's wake on the loop's own thread](/quest/m3/ffi-python-wake.md) - a wake on the event loop's thread resumes the future with no socket pair -- [Python's call cost](/quest/m3/ffi-python-calls.md) - a call on an object costs what BoltFFI's does, or the gap is explained - [A panic hook](/quest/m3/ts-panic-hook.md) - `set_hook`, `take_hook`, and `PanicHookInfo` translate, every panic running the hook - [A string Cow's variant](/quest/m3/ts-cow-str-variant.md) - matching a `Cow`'s `Borrowed` or `Owned`, and `to_mut`'s change of it, translate as std's diff --git a/quest/m3/ffi-python-calls.md b/quest/m3/ffi-python-calls.md deleted file mode 100644 index eea76cd79..000000000 --- a/quest/m3/ffi-python-calls.md +++ /dev/null @@ -1,15 +0,0 @@ -# [M] Python's call cost - -## Goal - -A call on an object through the exporter's [Python](/ffi/README.md) costs what -[BoltFFI's](/bench/ffi/README.md#boltffi-comparison) does, 63 ns, or `just ffi bench` -explains the gap. - -## Plan - -Found by [#407](https://github.com/moq-dev/rust2/pull/407), which measured about 125 ns on a -loaded host. Measure on a quiet one first, then profile where the extension spends the rest: -argument parsing, the handle's lookup, and the contract or GIL work per call. - -Public API: none.