From 73eb3424a0028f4f31a7ab943323050abe2486c8 Mon Sep 17 00:00:00 2001 From: Ali Hassan Date: Wed, 25 Mar 2026 15:52:22 +0500 Subject: [PATCH] src: use stack allocation for small string encoding Use stack-allocated buffers in StringBytes::Encode() for small inputs instead of heap-allocating via UncheckedMalloc for every call. Refs: https://github.com/nodejs/performance/issues/194 --- src/string_bytes.cc | 180 ++++++++++++++++++++++---------------------- 1 file changed, 91 insertions(+), 89 deletions(-) diff --git a/src/string_bytes.cc b/src/string_bytes.cc index 28432a82ad8835..dbcd5f58c68df4 100644 --- a/src/string_bytes.cc +++ b/src/string_bytes.cc @@ -145,12 +145,49 @@ class ExternString: public ResourceType { size_t length_; }; +typedef ExternString + ExternOneByteString; +typedef ExternString + ExternTwoByteString; + +template +static MaybeLocal EncodeOneByteString(Isolate* isolate, + size_t length, + EncodeFn encode) { + // 512B stack threshold: covers common small outputs (hex SHA-256/512, UUIDs). + // Larger thresholds were benchmarked + MaybeStackBuffer buf(length); + encode(buf.out()); + // Copy stack-backed data, but release heap-backed storage to V8. + if (buf.IsAllocated()) { + char* data = buf.out(); + buf.Release(); + return ExternOneByteString::New(isolate, data, length); + } + return String::NewFromOneByte(isolate, + reinterpret_cast(buf.out()), + v8::NewStringType::kNormal, + static_cast(length)); +} -typedef ExternString ExternOneByteString; -typedef ExternString ExternTwoByteString; - +template +static MaybeLocal EncodeTwoByteString(Isolate* isolate, + size_t char_length, + EncodeFn encode) { + // 256 uint16_t = 512 bytes on the stack, matching the one-byte + MaybeStackBuffer buf(char_length); + encode(buf.out()); + // Copy stack-backed data, but release heap-backed storage to V8. + if (buf.IsAllocated()) { + uint16_t* data = buf.out(); + buf.Release(); + return ExternTwoByteString::New(isolate, data, char_length); + } + return String::NewFromTwoByte(isolate, + buf.out(), + v8::NewStringType::kNormal, + static_cast(char_length)); +} template <> MaybeLocal ExternOneByteString::NewExternal( @@ -513,27 +550,23 @@ MaybeLocal StringBytes::Encode(Isolate* isolate, MaybeLocal val; switch (encoding) { - case BUFFER: - { - auto maybe_buf = Buffer::Copy(isolate, buf, buflen); - Local buf; - if (!maybe_buf.ToLocal(&buf)) { - isolate->ThrowException(node::ERR_MEMORY_ALLOCATION_FAILED(isolate)); - } - return buf; + case BUFFER: { + auto maybe_buf = Buffer::Copy(isolate, buf, buflen); + Local buf; + if (!maybe_buf.ToLocal(&buf)) { + isolate->ThrowException(node::ERR_MEMORY_ALLOCATION_FAILED(isolate)); } + return buf; + } case ASCII: buflen = keep_buflen_in_range(buflen); if (simdutf::validate_ascii_with_errors(buf, buflen).error) { // The input contains non-ASCII bytes. - char* out = node::UncheckedMalloc(buflen); - if (out == nullptr) { - isolate->ThrowException(node::ERR_MEMORY_ALLOCATION_FAILED(isolate)); - return MaybeLocal(); - } - nbytes::ForceAscii(buf, out, buflen); - return ExternOneByteString::New(isolate, out, buflen); + + return EncodeOneByteString(isolate, buflen, [buf, buflen](char* dst) { + nbytes::ForceAscii(buf, dst, buflen); + }); } else { return ExternOneByteString::NewFromCopy(isolate, buf, buflen); } @@ -557,14 +590,12 @@ MaybeLocal StringBytes::Encode(Isolate* isolate, isolate->ThrowException(ERR_STRING_TOO_LONG(isolate)); return MaybeLocal(); } - uint16_t* dst = node::UncheckedMalloc(u16size); - if (u16size != 0 && dst == nullptr) { - THROW_ERR_MEMORY_ALLOCATION_FAILED(isolate); - return MaybeLocal(); - } - size_t utf16len = simdutf::convert_valid_utf8_to_utf16( - buf, buflen, reinterpret_cast(dst)); - return ExternTwoByteString::New(isolate, dst, utf16len); + return EncodeTwoByteString( + isolate, u16size, [buf, buflen, u16size](uint16_t* dst) { + size_t written = simdutf::convert_valid_utf8_to_utf16( + buf, buflen, reinterpret_cast(dst)); + CHECK_EQ(written, u16size); + }); } val = @@ -583,77 +614,52 @@ MaybeLocal StringBytes::Encode(Isolate* isolate, case BASE64: { buflen = keep_buflen_in_range(buflen); size_t dlen = simdutf::base64_length_from_binary(buflen); - char* dst = node::UncheckedMalloc(dlen); - if (dst == nullptr) { - isolate->ThrowException(node::ERR_MEMORY_ALLOCATION_FAILED(isolate)); - return MaybeLocal(); - } - - size_t written = simdutf::binary_to_base64(buf, buflen, dst); - CHECK_EQ(written, dlen); - - return ExternOneByteString::New(isolate, dst, dlen); + return EncodeOneByteString(isolate, dlen, [buf, buflen, dlen](char* dst) { + size_t written = simdutf::binary_to_base64(buf, buflen, dst); + CHECK_EQ(written, dlen); + }); } case BASE64URL: { buflen = keep_buflen_in_range(buflen); size_t dlen = simdutf::base64_length_from_binary(buflen, simdutf::base64_url); - char* dst = node::UncheckedMalloc(dlen); - if (dst == nullptr) { - isolate->ThrowException(node::ERR_MEMORY_ALLOCATION_FAILED(isolate)); - return MaybeLocal(); - } - - size_t written = - simdutf::binary_to_base64(buf, buflen, dst, simdutf::base64_url); - CHECK_EQ(written, dlen); - - return ExternOneByteString::New(isolate, dst, dlen); + return EncodeOneByteString(isolate, dlen, [buf, buflen, dlen](char* dst) { + size_t written = + simdutf::binary_to_base64(buf, buflen, dst, simdutf::base64_url); + CHECK_EQ(written, dlen); + }); } case HEX: { buflen = keep_buflen_in_range(buflen); size_t dlen = buflen * 2; - char* dst = node::UncheckedMalloc(dlen); - if (dst == nullptr) { - isolate->ThrowException(node::ERR_MEMORY_ALLOCATION_FAILED(isolate)); - return MaybeLocal(); - } - size_t written = nbytes::HexEncode(buf, buflen, dst, dlen); - CHECK_EQ(written, dlen); - - return ExternOneByteString::New(isolate, dst, dlen); + return EncodeOneByteString(isolate, dlen, [buf, buflen, dlen](char* dst) { + size_t written = nbytes::HexEncode(buf, buflen, dst, dlen); + CHECK_EQ(written, dlen); + }); } case UCS2: { buflen = keep_buflen_in_range(buflen); size_t str_len = buflen / 2; if constexpr (IsBigEndian()) { - uint16_t* dst = node::UncheckedMalloc(str_len); - if (str_len != 0 && dst == nullptr) { - isolate->ThrowException(node::ERR_MEMORY_ALLOCATION_FAILED(isolate)); - return MaybeLocal(); - } - for (size_t i = 0, k = 0; k < str_len; i += 2, k += 1) { - // The input is in *little endian*, because that's what Node.js - // expects, so the high byte comes after the low byte. - const uint8_t hi = static_cast(buf[i + 1]); - const uint8_t lo = static_cast(buf[i + 0]); - dst[k] = static_cast(hi) << 8 | lo; - } - return ExternTwoByteString::New(isolate, dst, str_len); + return EncodeTwoByteString( + isolate, str_len, [buf, str_len](uint16_t* dst) { + for (size_t i = 0, k = 0; k < str_len; i += 2, k += 1) { + // The input is in *little endian*, because that's what Node.js + // expects, so the high byte comes after the low byte. + const uint8_t hi = static_cast(buf[i + 1]); + const uint8_t lo = static_cast(buf[i + 0]); + dst[k] = static_cast(hi) << 8 | lo; + } + }); } if (reinterpret_cast(buf) % 2 != 0) { - // Unaligned data still means we can't directly pass it to V8. - char* dst = node::UncheckedMalloc(buflen); - if (dst == nullptr) { - isolate->ThrowException(node::ERR_MEMORY_ALLOCATION_FAILED(isolate)); - return MaybeLocal(); - } - memcpy(dst, buf, buflen); - return ExternTwoByteString::New( - isolate, reinterpret_cast(dst), str_len); + return EncodeTwoByteString( + isolate, str_len, [buf, buflen](uint16_t* dst) { + memcpy(dst, buf, buflen); + }); } return ExternTwoByteString::NewFromCopy( isolate, reinterpret_cast(buf), str_len); @@ -675,15 +681,11 @@ MaybeLocal StringBytes::Encode(Isolate* isolate, // https://nodejs.org/api/buffer.html regarding Node's "ucs2" // encoding specification if constexpr (IsBigEndian()) { - uint16_t* dst = node::UncheckedMalloc(buflen); - if (dst == nullptr) { - isolate->ThrowException(node::ERR_MEMORY_ALLOCATION_FAILED(isolate)); - return MaybeLocal(); - } - size_t nbytes = buflen * sizeof(uint16_t); - memcpy(dst, buf, nbytes); - CHECK(nbytes::SwapBytes16(reinterpret_cast(dst), nbytes)); - return ExternTwoByteString::New(isolate, dst, buflen); + return EncodeTwoByteString(isolate, buflen, [buf, buflen](uint16_t* dst) { + size_t nbytes = buflen * sizeof(uint16_t); + memcpy(dst, buf, nbytes); + CHECK(nbytes::SwapBytes16(reinterpret_cast(dst), nbytes)); + }); } else { return ExternTwoByteString::NewFromCopy(isolate, buf, buflen); }