官方说一个破折号拖慢整段高亮,我在 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%
相关推荐
要开心吖ZSH20 分钟前
《通过SeaTunnel 把 MySQL 的数据搬进 Doris》番外篇:容量、切分与速度——Doris 能扛多少,比 MySQL 快多少
mysql·性能优化·doris·分区·分表·分桶
打工仔折腾 AI1 小时前
260+异构同步链路实战:如何用语义化自动翻译破解CDC DDL同步痛点
人工智能·后端·python·自然语言处理·性能优化·langchain·机器翻译
VimorphTech1 小时前
React 任务轮询,为什么不能只写一个 setInterval?
javascript·typescript
lexiangqicheng2 小时前
Docker的沙箱机制是怎样的?
前端·javascript
Sayai2 小时前
Java 进程 Unable to create new native thread 排查实战:Linux 线程数五层限制与 JVM -Xss 调优
java·linux·运维·jvm·性能优化
LCG米3 小时前
STM32 HardFault故障定位实测:压栈帧寄存器解析、CFSR故障源判读与出错地址回溯
前端·javascript·stm32·单片机·嵌入式
打工仔折腾 AI3 小时前
让时序预测真正参与决策:TimechoAI 工程化落地与管线设计实践
人工智能·后端·python·算法·机器学习·性能优化
持敬chijing4 小时前
白盒测试与代码审计-JS 数据逆向
javascript·网络·安全·web安全·网络安全·网络攻击模型·安全威胁分析
薛一半4 小时前
Redux实战:组件状态管理全解析
开发语言·前端·javascript·react.js
可乐ea4 小时前
微调提速 6 倍:Qwen 27B 从 7 小时 44 分到 1 小时 17 分
性能优化·模型微调·训练加速·fsdp2·数据打包