feat: zero-copy string access via ValueView and allocation-reuse APIs#1927
feat: zero-copy string access via ValueView and allocation-reuse APIs#1927bartlomieju wants to merge 4 commits intomainfrom
Conversation
…APIs Add ValueView::as_str() for true zero-copy &str access to ASCII strings, ValueView::to_cow_lossy() for zero-copy-when-possible string conversion, String::write_utf8_into() for allocation reuse, and a public latin1_to_utf8 SIMD-friendly transcoder utility. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Eliminates the utf8_length pre-scan by using ValueView for direct access to string contents. For one-byte strings this reduces from 2 FFI calls + 2 passes to 1 FFI call + 1 pass. Latin-1 transcoding uses latin1_to_utf8, and two-byte strings are transcoded directly into the stack buffer without an intermediate allocation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ow checker The nightly Rust compiler correctly rejects borrowing a by-value Local<'s, String> parameter since &*string creates a reference to the stack-local copy that is dropped at end of function. We recover the 's lifetime via pointer cast, which is safe because Local<'s, _> guarantees the V8 string is rooted in a HandleScope that lives for at least 's. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
kajukitli
left a comment
There was a problem hiding this comment.
lgtm
the zero-copy ValueView::as_str() / to_cow_lossy() additions make sense, and rewriting to_rust_cow_lossy() to use ValueView internally is the right optimization. the public latin1_to_utf8() helper also seems reasonable since the logic was already duplicated downstream.
one minor concern: write_utf8_into() still calls utf8_length() up front, so it's not fully on the one-pass path. that's probably fine because it needs to reserve capacity and reuse the existing String, but worth keeping in mind if the goal is to squeeze every last FFI call out of the hot path.
kajukitli
left a comment
There was a problem hiding this comment.
lgtm
the zero-copy ValueView::as_str() / to_cow_lossy() additions make sense, and rewriting to_rust_cow_lossy() to use ValueView internally is the right optimization. the public latin1_to_utf8() helper also seems reasonable since the logic was already duplicated downstream.
one minor concern: write_utf8_into() still calls utf8_length() up front, so it's not fully on the one-pass path. that's probably fine because it needs to reserve capacity and reuse the existing String, but worth keeping in mind if the goal is to squeeze every last FFI call out of the hot path.
Rewrite write_utf8_into to use ValueView internally, eliminating the separate utf8_length() FFI call. Now matches the same single-pass pattern used by to_rust_cow_lossy. The signature changes from &Isolate to &mut Isolate to satisfy ValueView's requirements; all existing callers pass &mut HandleScope which derefs to &mut Isolate, so this is source-compatible. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
ValueView::as_str()— true zero-copy&strfor ASCII strings (no alloc, no copy)ValueView::to_cow_lossy()— zero-copyCow::Borrowedfor ASCII, transcodedCow::Ownedfor Latin-1/UTF-16String::write_utf8_into()— write UTF-8 into an existingString, reusing its allocationlatin1_to_utf8()— SIMD-friendly Latin-1→UTF-8 transcoder (8-byte bulk processing)to_rust_cow_lossy()to useValueViewinternally — eliminatesutf8_lengthpre-scanAnalysis
Problem
Currently, accessing V8 string contents from Rust almost always requires a memory allocation + copy. The main methods are:
ValueView::data()&[u8]or&[u16](raw encoding)to_rust_string_lossy()Stringto_rust_cow_lossy()Cow<str>to_rust_string_lossyalways callsalloc::alloc()+ copy.to_rust_cow_lossydid two passes over the string data (utf8_lengthpre-scan +write_*), and still copies even when borrowing into a stack buffer.In
deno_core, the hot path (runtime/ops.rs::to_str()) uses an 8KB stack buffer withto_rust_cow_lossy, which avoids heap allocation for small strings but still did two passes and a copy.How ValueView changes the game
ValueView(V8'sv8::String::ValueView) flattens the string once and gives a direct pointer into V8's heap. For one-byte ASCII strings (the vast majority in practice — identifiers, property names, URLs, JSON keys), the bytes are already valid UTF-8, enabling true zero-copy access.New APIs
ValueView::as_str() -> Option<&str>— Zero-copy for ASCII one-byte strings. ReturnsNonefor Latin-1 non-ASCII or two-byte strings.ValueView::to_cow_lossy() -> Cow<'_, str>— Zero-copyBorrowedfor ASCII, single-pass transcode for everything else:Cow::Borrowed(&str)— no alloc, no copyCow::Ownedvialatin1_to_utf8(one pass, SIMD-friendly)Cow::Ownedviafrom_utf16_lossy(one pass)String::write_utf8_into(&self, scope, buf: &mut String)— Clears and fills an existingString, reusing its heap allocation. Enables patterns like thread-local reusable buffers with zero malloc after warmup.latin1_to_utf8(len, inbuf, outbuf) -> usize— Public utility for Latin-1→UTF-8 transcoding. Processes 8 bytes at a time with a single bitmask check (& 0x8080_8080_8080_8080), bulk-copying ASCII chunks and expanding non-ASCII bytes to 2-byte UTF-8 sequences. Previously this logic was duplicated indeno_core.to_rust_cow_lossyrewriteThe existing
to_rust_cow_lossyhas been rewritten to useValueViewinternally. Instead of callingutf8_length()(FFI pre-scan) +write_utf8_uninit_v2()(FFI copy), it now:ValueView(1 FFI call — flattens string, returns direct pointer)memcpyinto stack buffer (1 pass)latin1_to_utf8into stack buffer (1 pass)char::decode_utf16(1 pass, no intermediate allocation)This reduces from 2 FFI calls + 2 passes to 1 FFI call + 1 pass for the common one-byte case.
Full optimization roadmap
This PR implements items A, B, C, and G. The remaining items (D–F) are follow-up work in
deno_core.ValueView::as_str/to_cow_lossyString::write_utf8_into(&mut String)to_rust_cow_lossyvia ValueView internallyStringbufferto_str()with ValueView-based pathlatin1_to_utf8in rusty_v8Details on follow-up items
D. Thread-local reusable
Stringbuffer — The simplest high-impact change forto_string()/to_rust_string_lossy()callers in deno_core. Keep a thread-localStringwith a warm allocation, usewrite_utf8_intoto fill it, avoiding malloc in steady state.E. Buffer pool for owned strings — For cases where ownership is truly needed (the string escapes the current scope), a pool of pre-allocated
Vec<u8>buffers. Requires a custom string type that returns to the pool on drop. More invasive but eliminates malloc/free from the hot path entirely.F.
to_str()via ValueView in deno_core — The currentto_str()inruntime/ops.rscould useValueView+to_cow_lossy()instead ofto_rust_cow_lossywith an 8KB stack buffer. This eliminates the stack buffer for ASCII strings (the common case) and removes theutf8_lengthpre-scan for all strings. Caveat:ValueViewborrows&mut Isolate, so the returnedCowcan't outlive the view — works for#[string] s: &strbut not#[string] s: String.