mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-23 06:10:02 +08:00
Widen ASCII to UTF-16 sixteen bytes per step in Buffer.write
Buffer.write(text, "utf16le") zero-extended an ASCII run eight bytes per step with shifts and masks in 64-bit words. Where the target has vector unpacks (SSE2 on x86-64, NEON on AArch64) it now widens sixteen bytes per step: one load, a test of the high bits, and two unpacked stores. The word loop stays for the remaining bytes and for other targets. A scanner that fills a Uint16Array of code units this way, as tsc-ts does once per source file, spends about half as long in the copy.
This commit is contained in:
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5892-buffer-write-utf16le-runs.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -17,6 +17,11 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#if defined(__SSE2__)
|
||||
#include <emmintrin.h>
|
||||
#elif defined(__ARM_NEON) && defined(__aarch64__)
|
||||
#include <arm_neon.h>
|
||||
#endif
|
||||
|
||||
static bool scr_td_label_space(unsigned char c) {
|
||||
return c == 9 || c == 10 || c == 12 || c == 13 || c == 32;
|
||||
@@ -3302,13 +3307,30 @@ double scr_bytes_write_str(ScrBytes *b, const ScrStr *s, const ScrStr *enc,
|
||||
while (i < s->len && budget - written >= width) {
|
||||
#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
/* utf16le of an ASCII run: zero-extend each byte to a little-endian
|
||||
* unit, eight per step with two word stores (the generic loop below
|
||||
* stores byte by byte, because the source and destination may alias
|
||||
* as far as the compiler knows). */
|
||||
* unit, sixteen per step with vector unpacks where the target has them
|
||||
* and eight per step with two word stores otherwise (the generic loop
|
||||
* below stores byte by byte, because the source and destination may
|
||||
* alias as far as the compiler knows). */
|
||||
if (utf16) {
|
||||
uint8_t *d = b->data + o + written;
|
||||
size_t n = 0, room = (budget - written) / 16, left = (s->len - i) / 8;
|
||||
size_t steps = room < left ? room : left;
|
||||
#if defined(__SSE2__)
|
||||
const __m128i zero = _mm_setzero_si128();
|
||||
for (; n + 2 <= steps; n += 2) {
|
||||
__m128i v = _mm_loadu_si128((const __m128i *)(s->data + i + 8 * n));
|
||||
if (_mm_movemask_epi8(v)) break;
|
||||
_mm_storeu_si128((__m128i *)(d + 16 * n), _mm_unpacklo_epi8(v, zero));
|
||||
_mm_storeu_si128((__m128i *)(d + 16 * n + 16), _mm_unpackhi_epi8(v, zero));
|
||||
}
|
||||
#elif defined(__ARM_NEON) && defined(__aarch64__)
|
||||
for (; n + 2 <= steps; n += 2) {
|
||||
uint8x16_t v = vld1q_u8((const uint8_t *)s->data + i + 8 * n);
|
||||
if (vmaxvq_u8(v) & 0x80) break;
|
||||
vst1q_u8(d + 16 * n, vreinterpretq_u8_u16(vmovl_u8(vget_low_u8(v))));
|
||||
vst1q_u8(d + 16 * n + 16, vreinterpretq_u8_u16(vmovl_high_u8(v)));
|
||||
}
|
||||
#endif
|
||||
for (; n < steps; n++) {
|
||||
uint64_t w;
|
||||
memcpy(&w, s->data + i + 8 * n, 8);
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
// Buffer.write(text, "utf16le") over ASCII runs of every length around the
|
||||
// sixteen-byte widening steps (4732 covers the eight-byte ones): odd and even
|
||||
// offsets, length limits that cut a run, and non-ASCII, astral and
|
||||
// lone-surrogate units at every position of a 41-unit run.
|
||||
|
||||
function hex(bytes: Uint8Array): string {
|
||||
let out = "";
|
||||
for (let i = 0; i < bytes.length; i++) out += (bytes[i]! < 16 ? "0" : "") + bytes[i]!.toString(16);
|
||||
return out;
|
||||
}
|
||||
|
||||
function digest(bytes: Uint8Array): number {
|
||||
let h = 0x811c9dc5 | 0;
|
||||
for (let i = 0; i < bytes.length; i++) h = Math.imul(h ^ bytes[i]!, 0x01000193);
|
||||
return h >>> 0;
|
||||
}
|
||||
|
||||
const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$";
|
||||
function run(n: number): string {
|
||||
let s = "";
|
||||
for (let i = 0; i < n; i++) s += alphabet[i % alphabet.length];
|
||||
return s;
|
||||
}
|
||||
|
||||
let summary = 0;
|
||||
for (let n = 0; n <= 70; n++) {
|
||||
const text = run(n);
|
||||
for (const offset of [0, 1, 3, 16]) {
|
||||
const buf = Buffer.alloc(offset + 2 * n + 5, 0xee);
|
||||
const written = buf.write(text, offset, "utf16le");
|
||||
summary = (Math.imul(summary, 31) + written + digest(buf)) | 0;
|
||||
for (const limit of [0, 1, 15, 16, 17, 31, 32, 33]) {
|
||||
if (limit > 2 * n + 5) continue;
|
||||
const cut = Buffer.alloc(offset + 2 * n + 5, 0xee);
|
||||
const w = cut.write(text, offset, limit, "utf16le");
|
||||
summary = (Math.imul(summary, 31) + w + digest(cut)) | 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log("runs", summary);
|
||||
|
||||
for (const insert of ["é", "€", "😀", "\ud800", "\udc00", "\u0080", "ÿ"]) {
|
||||
let local = 0;
|
||||
for (let at = 0; at <= 40; at++) {
|
||||
const text = run(at) + insert + run(40 - at);
|
||||
const buf = Buffer.alloc(2 * text.length + 3, 0xee);
|
||||
const written = buf.write(text, 1, "utf16le");
|
||||
local = (Math.imul(local, 31) + written + digest(buf)) | 0;
|
||||
if (at === 17) console.log(JSON.stringify(insert), written, hex(buf.subarray(33, 41)));
|
||||
}
|
||||
console.log("insert", JSON.stringify(insert), local);
|
||||
}
|
||||
|
||||
// A code-unit array the way a scanner builds one, then read back.
|
||||
const source = run(1000) + "\n" + "const x = 1;\r\n".repeat(50);
|
||||
const units = new Uint16Array(source.length + 4);
|
||||
const n = Buffer.from(units.buffer).write(source, "utf16le");
|
||||
let same = n === 2 * source.length;
|
||||
for (let i = 0; i < source.length; i++) if (units[i] !== source.charCodeAt(i)) same = false;
|
||||
console.log("units", n, same, units[source.length], units[source.length + 3]);
|
||||
Reference in New Issue
Block a user