src: use simdutf for two-byte strings in UTF-8 writes · nodejs/node@32bb197 · GitHub
Skip to content

Commit 32bb197

Browse files
codebytereaduh95
authored andcommitted
src: use simdutf for two-byte strings in UTF-8 writes
StringBytes::Write() already used simdutf to encode one-byte strings as UTF-8 but sent every two-byte (UTF-16) string through v8::String::WriteUtf8V2(), which is several times slower. That path is behind Buffer.from(string), buf.write(), fs.write*() with string data and every string written to a libuv stream, and JSON.stringify() output is a two-byte string as soon as any value in the payload is outside Latin-1. Encode two-byte strings with simdutf as well whenever their UTF-8 form is guaranteed to fit in the target: well-formed input is converted directly, and input with unpaired surrogates is converted from a copy passed through simdutf::to_well_formed_utf16(), which replaces each unpaired surrogate with U+FFFD exactly like kReplaceInvalidUtf8 (this mirrors what TextEncoder already does). Writes that have to truncate at a character boundary keep using WriteUtf8V2(), so their output is byte-for-byte unchanged, and so do strings of up to 32 code units, for which V8 is already as fast (the same threshold TextEncoder uses). buf.write() of a 2 KiB two-byte string improves ~5x (astral-heavy and lone-surrogate strings ~3.5x and ~5x), Buffer.from() of a 64 KiB JSON string ~2.7x; one-byte strings are unaffected. Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com> PR-URL: #65324 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day> Reviewed-By: Daniel Lemire <daniel@lemire.me> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent f33dba7 commit 32bb197

3 files changed

Lines changed: 193 additions & 1 deletion

File tree

Lines changed: 36 additions & 0 deletions

src/string_bytes.cc

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,9 +310,37 @@ size_t StringBytes::Write(Isolate* isolate,
310310
input_view.length(),
311311
buf,
312312
buflen);
313-
} else {
313+
} else if (input_view.length() <= 32) {
314+
// V8 is as fast for tiny strings (same threshold TextEncoder uses).
314315
nbytes = str->WriteUtf8V2(
315316
isolate, buf, buflen, String::WriteFlags::kReplaceInvalidUtf8);
317+
} else {
318+
// Use simdutf for two-byte strings as well whenever the UTF-8 form
319+
// is guaranteed to fit; truncating writes (which must stop at a
320+
// character boundary) keep going through V8 so that their output
321+
// stays byte-for-byte identical.
322+
const char16_t* data =
323+
reinterpret_cast<const char16_t*>(input_view.data16());
324+
const size_t length = input_view.length();
325+
MaybeStackBuffer<char16_t, 1024> well_formed;
326+
if (!simdutf::validate_utf16(data, length)) {
327+
// Unpaired surrogates: encode a copy in which each of them has been
328+
// replaced with U+FFFD, which is what kReplaceInvalidUtf8 produces.
329+
well_formed.AllocateSufficientStorage(length);
330+
simdutf::to_well_formed_utf16(data, length, well_formed.out());
331+
data = well_formed.out();
332+
}
333+
// A UTF-16 code unit never expands to more than 3 UTF-8 bytes, so
334+
// 3 * length is what StorageSize() hands most callers; only compute
335+
// the exact length when the buffer is smaller than that.
336+
if (buflen >= 3 * length ||
337+
buflen >= simdutf::utf8_length_from_utf16(data, length)) {
338+
nbytes = simdutf::convert_utf16_to_utf8(data, length, buf);
339+
} else {
340+
// Does not fit: let V8 truncate at a character boundary.
341+
nbytes = str->WriteUtf8V2(
342+
isolate, buf, buflen, String::WriteFlags::kReplaceInvalidUtf8);
343+
}
316344
}
317345
break;
318346

Lines changed: 128 additions & 0 deletions

0 commit comments

Comments
 (0)