官方说一个破折号拖慢整段高亮,我在 Node 里验证了一遍:机制是真的,2.8 倍没跑出来

近日 Anthropic 发了篇工程复盘,讲他们 8 月怎么用两周把 claude.ai 和桌面端提速 3.1 倍。这种复盘一般是一堆缓存优化的流水账,但这篇里藏着一个特别离谱的瓶颈:破折号。

官方的原话大意是:Claude 高亮代码块时页面会冻住将近一秒,排查到最后发现元凶是 em dash------只要回复的 markdown 里混进一个 Latin-1 装不下的字符(破折号、弯引号,甚至一个汉字),V8 就把整个字符串存成 UTF-16 双字节,所有语法高亮的正则全被拖进双字节慢路径。修复只用了 20 行代码:高亮前把代码块拷贝进单字节字符串,首个 TypeScript 代码块的高亮时间从 1.0 秒降到 0.35 秒。

36氪上的解读标题起得更直接:中文用户全线中招。这话说得没错,按照这个机制,汉字是中文用户天生的。 机制听起来很清楚,但"一个字符拖慢整串正则"这个结论我还是想亲手摸一摸。理由有二:一是这直接关系到我平时写的内容渲染代码;二是 1.0 秒降到 0.35 秒这个 2.8 倍的差距,说实话大得让我有点怀疑------正则扫描同一份内容,就因为字符串宽了一倍,能差这么多?

先验证机制:一个汉字够不够

我的环境是 Node 22(v22.23.2,V8 12.4),后面所有实验都是本机跑的,完整脚本贴在文末,可以直接复现。 V8 内部字符串有两种存储格式这件事,文档里写得不显眼,但可以用调试原语直接看。写三段内容几乎一样的字符串,用 --allow-natives-syntax 的 %DebugPrint 看它们的内部类型:

ini 复制代码
// node --allow-natives-syntax
const ascii = "a".repeat(1000);
const han   = "这" + "a".repeat(999);
const em    = "---" + "a".repeat(999);
%DebugPrint(ascii); // CONS_ONE_BYTE_STRING_TYPE
%DebugPrint(han);   // CONS_TWO_BYTE_STRING_TYPE
%DebugPrint(em);    // CONS_TWO_BYTE_STRING_TYPE

结论和官方复盘完全一致:纯 ASCII 的串是 one-byte,塞进一个汉字或者一个破折号,整串变 two-byte。注意是整串------不是"这个字符"变宽了,是从第一个字节到最后一个字节全部按 16 位存。V8 的规则就是这么简单粗暴:内容里有一个 Latin-1 装不下的字符,整个字符串升级成双字节,没有任何例外。 机制实锤了。接下来是正题:这个 two-byte 串,真的会把正则拖慢 2.8 倍吗?

然后我就翻车了

我构造了一份"AI 回复"的 fixture:一段正文加几百个 TypeScript 代码块,凑到 64 万字符。代码块内容完全相同,唯一变量是正文里那一个字符------ASCII 版、em dash 版、汉字版。高亮器用一组模拟 Shiki/TextMate 的正则(注释、字符串、关键字、数字四条规则)在整段文本上扫。

bash 复制代码
config             median
A/ascii full scan  3.06ms
A/emdash full scan 3.03ms
A/han   full scan  3.00ms

0.98 倍。别说 2.8 倍,连 10% 都没有。 我第一反应是实验哪里错了。最可疑的是负载太轻:4 条正则扫 64 万字符只要 3 毫秒,这么小的数据集全部待在 CPU 缓存里,内存带宽翻倍这种事根本显不出来。而 Shiki 挂载的 TypeScript grammar 有几百条规则,官方测的那个代码块高亮一次要 40 到 100 毫秒------负载规模差着一两个数量级。 所以我加码重测。规则扩到 30 条,混进去懒惰量词、括号配对回溯、前后行锚点、前瞻后顾这些 grammar 里的典型结构;文本扩到 140 万字符;再把"每位置尝试多分支"的 TextMate 工作模式压成一个交替分支的复合大正则。最后干脆脱离缓存,把文本放到 64MB 再扫一遍。

python 复制代码
1/5MB   30 rules full scan    ascii 64.4ms | han 64.0ms   ratio 0.99x
2/5MB   alt-grammar scan      ascii 29.4ms | han 29.0ms   ratio 0.99x
3/64MB  literal scan          ascii  8ms   | han  6ms     ratio 0.71x
4/64MB  char-class scan       ascii 144ms  | han 145ms    ratio 1.00x

还是等速。64MB 字面量扫描那次 two-byte 甚至读数更快------单轮只有几毫秒,这个波动就是噪声,不足以当结论,但至少说明没有稳定劣势。 到这里我可以确认:在我这台机器的 Node 22 上,V8 的 two-byte 正则路径和 one-byte 就是等速的。30 条规则、64MB 文本、多种规则形态,怎么换负载都跑不出官方那个 2.8 倍。

那 2.8 倍是哪来的

翻车之后我花了不少时间想这个问题,先说说我个人的判断。 环境差异是最明显的:官方测试在 headless Chrome 里跑,Chromium 的 V8 版本比我这个 Node 22 新了不止一代;claude.ai 用的 Shiki 有 JS 引擎和 WASM oniguruma 两种后端,36氪复现说 claude.ai 用的是 V8 自带正则,那大概率是 JS 引擎路线,但这块官方复盘没有明说。

负载差异更实质。真实 TS grammar 每个块 40 到 100 毫秒的耗时,意味着几百条规则加深度回溯的持续压榨------在这种量级下,哪怕每字符只差几个纳秒,乘上规则数和回溯步数也会被放大。我的 30 条规则模拟不了这个密度。

还有一条我觉得被低估的:官方那个 1.0 秒,未必全是"正则扫描"。一次高亮的调用链里,markdown 解析、代码块切分、字符串反复 slice 和拼接都在跑,two-byte 串在这条链路的每一环都是双倍内存带宽,末端 tokenize 只是肉眼可见的那一环。修复把代码块归一成 one-byte 之后,整条链路都受益,账不能全算在正则头上。

所以我的结论是:机制(编码升级、慢路径的存在)是真的,但"2.8 倍"这个数字强烈依赖官方的环境和负载,别拿去外推。谁要是手头有 Chrome 141 的环境,跑一下我文末的脚本,我很想知道结果。

意外收获:比性能差更有用的是这个坑

重测的过程里,我盯着 V8 的字符串类型输出看了很久,发现一个官方复盘一句带过、但实际很隐蔽的细节。 官方修复的原文是"把代码块拷贝进单字节字符串"。为什么是拷贝?slice 一下不行吗?毕竟代码块内容本身是纯 ASCII 的,slice 出来理论上可以更省。

我测了:

ini 复制代码
const big    = "正文。" + "a".repeat(5000);  // 含汉字,two-byte
const sliced = big.slice(3);                 // 内容全是 ASCII
%DebugPrint(sliced);  // SLICED_TWO_BYTE_STRING_TYPE ------ 还是双字节!
const copied = Buffer.from(sliced, "utf8").toString("latin1");
%DebugPrint(copied);  // SEQ_ONE_BYTE_STRING_TYPE ------ 这才是单字节

V8 的 slice 对足够长的子串生成 SlicedString,共享母串的存储表示。母串是 two-byte,切出来的子串哪怕内容全是 ASCII,内部照样是 two-byte。这也是我重测实验里专门加的一组对照:从 two-byte 整串 slice 出代码块再扫描,和拷贝后扫描比,只快 3.8%------因为它根本没有变成 one-byte。 也就是说,如果你试图用"把代码块切出来单独处理"来绕开编码问题,等于什么都没做。字符串的编码跟着它的存储单元走,不跟着内容走。想归一化编码,只能真正地拷贝一份新串。

这个坑我觉得比"two-byte 正则慢"本身更有实用价值。做内容渲染、编辑器、高亮组件的人,从一个大文本里 slice 出片段去处理的代码模式太常见了,几乎没人会意识到编码表示也跟着 slice 进来了。想检查自己手里的串是什么表示,node --allow-natives-syntax 加 %DebugPrint 一眼就能看到。

写到最后

把这次的东西归拢一下。机制层面官方没说错,一个汉字就够,整串升级,这个我拿 %DebugPrint 看得清清楚楚。slice 陷阱算这趟最大的新鲜货。至于 2.8 倍,我复现不出来,我猜是 Chrome 环境、真实 grammar 负载和整条解析链路叠加出来的,单一因素都凑不齐,但这话我没有证据,只有倾向。

至于破折号这个梗,Anthropic 的处理方式我已经笑过了:模型爱写破折号的毛病一点没改,直接改网页底层去适应它。写手们忙着删破折号防 AI 检测的时候,大概谁也想不到 Claude 自己的破折号先把网页卡了------这应该是"AI 味儿"第一次造成实打实的性能事故。

demo 不用找我要,两个 benchmark 脚本加 %DebugPrint 验证,完整贴在文末折叠区,Node 18 以上都能直接跑。如果你在浏览器里跑出了不一样的数字,评论区贴一下,我挺好奇这个 2.8 倍到底藏在哪一层。

完整实验脚本与原始输出

脚本一:v8_encoding_bench.js(首轮:机制验证 + 4 条规则扫描)

scss 复制代码
// v8_encoding_bench.js
// 复现 Anthropic 官方复盘(2026-09-24 发布)的 V8 编码性能问题:
//   场景:AI 回复 = 正文 + 多个代码块;claude.ai 用 Shiki(底层 V8 正则)给代码上色
//   V8 规则:字符串内容混入任何一个 > U+00FF 的字符(em dash / 汉字),整串按 UTF-16 双字节存储
//   - Latin-1 单字节串:正则走 one-byte 快速路径
//   - UTF-16 双字节串:正则走 two-byte 慢路径
//   官方修复(20 行):高亮前把代码块拷贝进单字节字符串,首个 TS 块 1.0s -> 0.35s
//
// 本脚本验证四件事:
//   A) 同一组高亮正则,整串含 1 个汉字 / 1 个 em dash vs 纯 ASCII,耗时差多少
//   D) 从双字节大串 slice 出纯 ASCII 子串,是否仍是慢路径(官方必须"拷贝"而非"截取"的原因)
//   E) 把代码块重新拷贝成单字节串再扫描(官方修复模拟),是否恢复快路径
//   附) 官方修复带来的首块收益在本机的等比复现

const fs = require('fs');

// ---------- 模拟 Shiki/TextMate 的正则 tokenize(4 条规则,简化 grammar) ----------
const RULES = [  ///[^\n]*|/*[\s\S]*?*//g,                                              // 注释
  /'(?:[^'\\n]|\.)*'|"(?:[^"\\n]|\.)*"|`(?:[^`\]|\.)*`/g,                 // 字符串
  /\b(?:const|let|function|return|if|else|for|while|import|export|from|interface|type|new|await|async|throw|continue|class|extends|implements)\b/g, // 关键字
  /\b0x[0-9a-fA-F]+\b|\b\d+(?:.\d+)?\b/g,                                      // 数字
];

function scan(str) {
  let hits = 0;
  for (const re of RULES) {
    re.lastIndex = 0;
    let m;
    while ((m = re.exec(str)) !== null) {
      hits++;
      if (m.index === re.lastIndex) re.lastIndex++; // 防空匹配死循环
    }
  }
  return hits;
}

// ---------- 生成模拟回复:正文 + N 个 TypeScript 代码块(真实感 fixture) ----------
const CODE_TEMPLATE = `export interface Invoice {
  id: string;
  amountCents: number;
  currency: "CNY" | "USD";
  status: InvoiceStatus;
  createdAt: Date;
  lines: InvoiceLine[];
}

export type InvoiceStatus = "draft" | "issued" | "paid" | "void";

export function summarize(invoices: Invoice[]): Record<string, number> {
  const totals: Record<string, number> = {};
  for (const inv of invoices) {
    if (inv.status === "void") continue; // 作废单据不参与统计
    const key = inv.currency + "-" + inv.createdAt.getFullYear();
    totals[key] = (totals[key] ?? 0) + inv.amountCents; // 保留分精度,避免浮点误差
  }
  return totals;
}

export async function fetchInvoices(page: number, size = 50): Promise<Invoice[]> {
  const res = await fetch("/api/invoices?page=" + page + "&size=" + size);
  if (!res.ok) {
    throw new Error("HTTP " + res.status + ": unable to load invoices");
  }
  const data = (await res.json()) as { items: Invoice[] };
  return data.items.map((raw) => ({ ...raw, createdAt: new Date(raw.createdAt) }));
}
`;

function buildReply(mode, blocks) {
  // 三个版本的正文长度接近,唯一差异是那"一个字符"
  const bodyMap = {
    ascii:  'Here is the summary of this change: every endpoint stays backward compatible, details in the code below.\n',
    emdash: 'Here is the summary of this change --- every endpoint stays backward compatible, details in the code below.\n',
    han:    '这是本次改动的说明:所有接口都做了向后兼容处理,具体细节见下面的代码。\n',
  };
  let code = '';
  for (let i = 0; i < blocks; i++) {
    code += '// ---- block ' + i + ' (generated fixture) ----\n' + CODE_TEMPLATE + '\n';
  }
  return { body: bodyMap[mode], code, full: bodyMap[mode] + code };
}

// ---------- 计时框架:warmup 2 轮 + 计时 7 轮取中位数 ----------
function bench(label, fn, rounds = 7) {
  for (let i = 0; i < 2; i++) fn(); // warmup(触发 JIT 与字符串 flatten)
  const times = [];
  let hits = 0;
  for (let i = 0; i < rounds; i++) {
    const t0 = process.hrtime.bigint();
    hits = fn();
    const t1 = process.hrtime.bigint();
    times.push(Number(t1 - t0) / 1e6);
  }
  times.sort((a, b) => a - b);
  const med = times[Math.floor(times.length / 2)];
  return { label, median: med, min: times[0], max: times[times.length - 1], hits };
}

const BLOCKS = parseInt(process.env.BLOCKS || '600', 10); // 600 块 x ~1KB ≈ 660KB 代码文本

console.log('Node', process.version, '| fixture:', BLOCKS, 'blocks,',
  Buffer.byteLength(buildReply('ascii', BLOCKS).full, 'utf8'), 'bytes total');
console.log('rules:', RULES.length, '| rounds: 2 warmup + 7 timed, median reported');
console.log('');

const results = [];

// A) 整串扫描:还原官方 bug 场景(高亮正则在整段回复上跑)
for (const mode of ['ascii', 'emdash', 'han']) {
  const { full } = buildReply(mode, BLOCKS);
  results.push(bench('A/' + mode.padEnd(7) + ' full scan', () => scan(full)));
}

// D) slice 出代码块(模拟 markdown 解析切出 fenced block)------验证子串保留母串编码
{
  const { full } = buildReply('han', BLOCKS);
  const start = full.indexOf('// ---- block 0');
  const sliced = full.slice(start); // 内容全 ASCII,但来自 two-byte 母串
  results.push(bench('D/han     sliced copy', () => scan(sliced)));
}

// E) 官方修复模拟:代码块重新拷贝为单字节串(内容全 ASCII,latin1 重编码 = 强制 one-byte)
{
  const { full } = buildReply('han', BLOCKS);
  const start = full.indexOf('// ---- block 0');
  const copied = Buffer.from(full.slice(start), 'utf8').toString('latin1'); // 一次性拷贝
  results.push(bench('E/han     copied latin1', () => scan(copied)));
}

console.log('config                          median      min       max   hits');
for (const r of results) {
  console.log(
    r.label.padEnd(28),
    r.median.toFixed(2).padStart(7) + 'ms',
    r.min.toFixed(2).padStart(8) + 'ms',
    r.max.toFixed(2).padStart(8) + 'ms',
    String(r.hits).padStart(7)
  );
}

// ---------- 汇总 ----------
const get = (k) => results.find((r) => r.label.startsWith(k));
const ascii = get('A/ascii'), han = get('A/han'), em = get('A/emdash');
const sliced = get('D/han'), copied = get('E/han');
console.log('');
console.log('== 汇总 ==');
console.log('1 个汉字拖慢整串倍率        :', (han.median / ascii.median).toFixed(2) + 'x');
console.log('1 个 em dash 拖慢整串倍率   :', (em.median / ascii.median).toFixed(2) + 'x');
console.log('slice 后仍是慢路径?         :', sliced.median > ascii.median * 1.5 ? '是(' + (sliced.median / ascii.median).toFixed(2) + 'x)' : '否');
console.log('拷贝为单字节后恢复倍率      :', (copied.median / ascii.median).toFixed(2) + 'x(对照 ascii)');
console.log('修复收益(slice vs copy)   :', ((1 - copied.median / sliced.median) * 100).toFixed(1) + '% off');

// ---------- 官方首块场景等比复现 ----------
// 官方数字:首个 TS 块高亮 1.0s -> 0.35s(TextMate grammar 几百条规则,块几十 KB)
// 本机用 4 条简化规则,取单块做等比演示(规则数差距大,只验证方向,不宣称对齐官方绝对值)
{
  const { code } = buildReply('han', 1); // 单块 + 汉字正文
  const single = code;
  const singleAscii = buildReply('ascii', 1).code;
  const rSlow = bench('single/slow (han context)', () => scan(single), 15);
  const rFast = bench('single/fast (copied)     ', () => scan(Buffer.from(single, 'utf8').toString('latin1')), 15);
  const rRef  = bench('single/ref   (pure ascii)', () => scan(singleAscii), 15);
  console.log('');
  console.log('== 单块等比演示(官方场景缩放:1.0s->0.35s ≈ 65% off)==');
  console.log('慢路径(汉字正文上下文):', rSlow.median.toFixed(3) + 'ms | 快路径(拷贝后):', rFast.median.toFixed(3) + 'ms | 纯ASCII参照:', rRef.median.toFixed(3) + 'ms');
  console.log('本机修复收益:', ((1 - rFast.median / rSlow.median) * 100).toFixed(1) + '% off');
}

脚本二:v8_encoding_bench2.js(加码:30+ 条规则、复合大正则、64MB 扫描)

javascript 复制代码
// v8_encoding_bench2.js --- 修正负载后的复现实验
// 已确认(%DebugPrint):
//   ascii 串 -> ONE_BYTE;含 1 汉字/em dash -> TWO_BYTE
//   two-byte 母串 slice 出纯 ASCII 子串 -> SLICED_TWO_BYTE(仍是慢表示!)
//   重新拷贝 -> SEQ_ONE_BYTE(官方修复原理)
// 本轮修正负载:简单 4 规则在 cache 内的 646KB 上无差异,改用
//   (1) grammar 风格复合交替大正则(模拟 TextMate 多分支尝试)
//   (2) 40 条混合复杂度规则全扫(放大 per-char 成本差)
//   (3) 64MB 线性扫描(脱离 cache,验证内存带宽差异)

const RULES = [
  ///[^\n]*|/*[\s\S]*?*//g,
  /'(?:[^'\\n]|\.)*'|"(?:[^"\\n]|\.)*"|`(?:[^`\]|\.)*`/g,
  /\b(?:const|let|function|return|if|else|for|while|import|export|from|interface|type|new|await|async|throw|continue|class|extends|implements)\b/g,
  /\b0x[0-9a-fA-F]+\b|\b\d+(?:.\d+)?\b/g,
  /^\s*(?:[-*+]|\d+[.)])\s+/gm,                 // 列表标记(行锚点)
  /^(?:#{1,6})\s+\S.*$/gm,                      // 标题
  /${(?:[^{}]|{[^{}]*})*}/g,               // 模板插值(懒惰回溯)
  /\b(?:if|while|for)\s*((?:[^()]|([^()]*))*)/g, // 括号配对回溯
  /[A-Za-z_$][\w$]*(?=\s*()/g,                 // 函数调用名(前瞻)
  /(?<=.)[A-Za-z_$][\w$]*/g,                   // 属性访问(后顾)
  /\b(?:true|false|null|undefined|this)\b/g,
  /=>|...|??||||&&/g,
  /\b[A-Z][A-Za-z0-9_]*\b/g,                    // 类型名
  /@(?:\w+)/g,                                  // 装饰器
  /\b(?:public|private|protected|readonly|static)\b/g,
  /\b(?:string|number|boolean|void|never|unknown|any)\b/g,
  /[{}[]()]/g,                                 // 括号
  /[+-*/%=<>!&|^~?:]+/g,                       // 运算符
  /\bimport\s+(?:type\s+)?{[^}]*}\s+from\s+['"][^'"]+['"]/g,
  /\bexport\s+(?:default\s+)?(?:async\s+)?(?:function|class|const|let|interface|type)\b/g,
  /:\s*[A-Za-z_$][\w$]*(?:[])?(?:\s*|\s*[A-Za-z_$][\w$]*)*/g, // 类型注解
  /\bawait\s+\w/g,
  /https?://[^\s'"]+/g,
  /\berr(?:or)?\b/gi,
  /\btest(?:ing|s)?\b/gi,
  /\b(?:foo|bar|baz|qux)\b/g,
  /.\w+/g,
  /,(?=\s*[^\s])/g,
  /\s{2,}/g,
  /[^\x00-\x7F]+/g,                             // 非 ASCII 片段
];

// grammar 风格复合大正则:交替分支(多分支尝试 = TextMate 引擎每位置的成本结构)
const GRAMMAR_ALT = new RegExp(RULES.slice(0, 12).map(r => r.source).join('|'), 'gm');

function scanAll(str) {
  let hits = 0;
  for (const re of RULES) { re.lastIndex = 0; let m; while ((m = re.exec(str))) { hits++; if (m.index === re.lastIndex) re.lastIndex++; } }
  return hits;
}
function scanAlt(str) { let hits = 0; GRAMMAR_ALT.lastIndex = 0; let m; while ((m = GRAMMAR_ALT.exec(str))) { hits++; if (m.index === GRAMMAR_ALT.lastIndex) GRAMMAR_ALT.lastIndex++; } return hits; }

function bench(label, fn, rounds) {
  for (let i = 0; i < 2; i++) fn();
  const t = []; let hits = 0;
  for (let i = 0; i < rounds; i++) { const a = process.hrtime.bigint(); hits = fn(); t.push(Number(process.hrtime.bigint() - a) / 1e6); }
  t.sort((x, y) => x - y);
  return { label, median: t[Math.floor(t.length / 2)], min: t[0], hits };
}

// ---------- 负载 1:5MB 文本 × 30 条规则全扫 ----------
function build(mode, blocks) {
  const bodyMap = {
    ascii: 'Here is the summary of this change: every endpoint stays backward compatible, details in the code below.\n',
    emdash: 'Here is the summary of this change --- every endpoint stays backward compatible, details in the code below.\n',
    han: '这是本次改动的说明:所有接口都做了向后兼容处理,具体细节见下面的代码。\n',
  };
  let code = '';
  for (let i = 0; i < blocks; i++) code += '// ---- block ' + i + ' (fixture) ----\nexport interface Invoice' + i + ' {\n  id: string;\n  amountCents: number;\n  currency: "CNY" | "USD";\n  status: "draft" | "issued" | "paid";\n  createdAt: Date;\n}\nexport function summarize' + i + '(xs: Invoice' + i + '[]): Record<string, number> {\n  const totals: Record<string, number> = {};\n  for (const inv of xs) {\n    if (inv.status === "void") continue; // 作废单据不参与统计\n    totals[inv.currency] = (totals[inv.currency] ?? 0) + inv.amountCents;\n  }\n  return totals;\n}\n';
  return bodyMap[mode] + code;
}

const M5 = build('ascii', 2800); // ~5MB
console.log('fixture 5MB:', (M5.length / 1e6).toFixed(1) + 'M chars');
const han5 = build('han', 2800);
const r1a = bench('1/5MB 30rules ascii', () => scanAll(M5), 5);
const r1h = bench('1/5MB 30rules han  ', () => scanAll(han5), 5);
console.log(r1a.label, r1a.median.toFixed(1) + 'ms |', r1h.label, r1h.median.toFixed(1) + 'ms | ratio', (r1h.median / r1a.median).toFixed(2) + 'x');

const r2a = bench('2/5MB alt-grammar ascii', () => scanAlt(M5), 5);
const r2h = bench('2/5MB alt-grammar han  ', () => scanAlt(han5), 5);
console.log(r2a.label, r2a.median.toFixed(1) + 'ms |', r2h.label, r2h.median.toFixed(1) + 'ms | ratio', (r2h.median / r2a.median).toFixed(2) + 'x');

// ---------- 负载 2:64MB 线性扫描(脱离 cache) ----------
const big64 = build('ascii', 36000); // ~64MB
const big64h = build('han', 36000);
console.log('fixture 64MB:', (big64.length / 1e6).toFixed(1) + 'M chars');
const re1 = /'/g; // 简单单字符扫描:纯带宽
const re2 = /function/g; // 字面量搜索
function linScan(s, re) { re.lastIndex = 0; let c = 0; while (re.exec(s)) c++; return c; }
const r3a = bench('3/64MB literal ascii', () => linScan(big64, re2), 3);
const r3h = bench('3/64MB literal han  ', () => linScan(big64h, re2), 3);
console.log(r3a.label, r3a.median.toFixed(0) + 'ms |', r3h.label, r3h.median.toFixed(0) + 'ms | ratio', (r3h.median / r3a.median).toFixed(2) + 'x');

const r4a = bench('4/64MB class-scan ascii', () => linScan(big64, /[\s;]/g), 3);
const r4h = bench('4/64MB class-scan han  ', () => linScan(big64h, /[\s;]/g), 3);
console.log(r4a.label, r4a.median.toFixed(0) + 'ms |', r4h.label, r4h.median.toFixed(0) + 'ms | ratio', (r4h.median / r4a.median).toFixed(2) + 'x');

// ---------- slice vs copy 的成本差(官方修复的有效性) ----------
{
  const full = han5;
  const start = full.indexOf('// ---- block 0');
  const sliced = full.slice(start);
  const copied = Buffer.from(sliced, 'utf8').toString('latin1');
  const rs = bench('5/5MB scan sliced(two-byte)', () => scanAll(sliced), 5);
  const rc = bench('5/5MB scan copied(one-byte)', () => scanAll(copied), 5);
  console.log(rs.label, rs.median.toFixed(1) + 'ms |', rc.label, rc.median.toFixed(1) + 'ms | copy wins', ((1 - rc.median / rs.median) * 100).toFixed(1) + '%');
}

原始输出 v8_bench_output.txt:

python 复制代码
fixture 5MB: 1.4M chars
1/5MB 30rules ascii 64.4ms | 1/5MB 30rules han   64.0ms | ratio 0.99x
2/5MB alt-grammar ascii 29.4ms | 2/5MB alt-grammar han   29.0ms | ratio 0.99x
fixture 64MB: 17.6M chars
3/64MB literal ascii 8ms | 3/64MB literal han   6ms | ratio 0.71x
4/64MB class-scan ascii 144ms | 4/64MB class-scan han   145ms | ratio 1.00x
5/5MB scan sliced(two-byte) 62.2ms | 5/5MB scan copied(one-byte) 59.8ms | copy wins 3.8%
相关推荐
默_笙1 小时前
🚗 把小说装进数据库了:我的第一个 RAG,和它的五个硬伤
前端·javascript
像我这样帅的人丶你还1 小时前
📱iPhone Duo 开屏动画咋实现的?
javascript·webgl·three.js
Leslie1651 小时前
把 SQL 性能诊断接入模型 API:从谓词分析到可验证优化
javascript
数据库小学妹1 小时前
MySQL深分页优化:LIMIT大偏移的根因分析与五种解法对比
数据库·mysql·性能优化
gnip1 小时前
UniApp 内嵌 H5 通信全攻略
前端·javascript
用户6802659051191 小时前
企业电脑统一管理怎么做?2026企业终端统一管理方法与工具推荐
javascript·后端·面试
yunwei371 小时前
使用 eBPF 跟踪 Nginx 请求
linux·后端·性能优化
yunwei371 小时前
使用 eBPF 跟踪 MySQL 查询
linux·后端·性能优化
小凯在掘金1 小时前
为什么要有访问器? 你不知道的对象属性
前端·javascript