近日 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%