用 Zig 语言写的 JS/TS 词法器,探索如何利用 SIMD 提升性能。
simdjson 证明「结构性跳过」式的向量化能让解析的 I/O 密集阶段快一个数量级,但主流 JS/TS 工具链的词法解析至今仍以逐字节标量循环为主。本项目探索 JS/TS词法解析如何利用 SIMD 拿到收益,瓶颈在哪里。项目同时保持多个架构不同的实现,对比这些架构在不同样本上的性能表现和原因,探索通用的优化策略或混合策略。
核心目标是将字节流转化为 token 流的整体效率,token 合法性的验证留给 parser、linter 等按需开启或自行处理。为追求性能和容错,可以做合理取舍,比如实践中罕见的 token 排列可由 parser 发起重扫描或二次切分,对错误输入可以给出不符合 spec 的切分结果。详见 docs/goals。当前的具体取舍见 docs/tradeoff。
项目以架构矩阵方式并行演化:src/variants/ 下多个架构共享语义层、各自优化,每次 push 到 main 由 CI 自动跑全变体正确性校验 + 矩阵基准,趋势归档 bench-reports 分支。详见 docs/architecture。当前四个并行变体:
scalar:全标量单阶段,逐字节决策,作为基线参照(对应 yuku-old 形态)。jump_vec:单阶段 + SIMD 长跳跃(空白块扫、注释 trivia 快跳),当前 9/10 样本为矩阵最快、真实样本几何平均追平 yuku-main。two_phase:阶段 1 纯 SIMD 无分支地为每个字节建立分类位平面,推导 token 候选起点位图(换行统计同趟完成);阶段 2@ctz迭代候选起点,按首字节类别码分发贪心消费。bitmap:oxc_lexer 式六趟位图流水线(classify 一轮产多种每块位图 → carve → coalesce → compress 从位图批量压出 lexeme 流),classify 趟在 aarch64 走 NEON nibble LUT;作为 oxc_bitmap 的同族参照。
所有向量化集中在 src/simd.zig,用 Zig @Vector 表达、编译器自动降到 AVX2 / NEON,不写 intrinsics(例外:bitmap 变体的 classify 趟在 aarch64 走 NEON inline asm 的 nibble LUT)。
zig build test # 跑测试
zig build run -- src/scanner.zig # 扫描文件,输出统计
zig build run -- --dump some.js # 打印每个 lexeme(含行号)
zig build --release=fast run -- --bench=50 big.js # 吞吐基准
scripts/check.sh # 正确性校验(全变体 tsc 差分测试)$ zig build run -- src/scanner.zig
src/scanner.zig: 69633 bytes, 13019 lexemes, 1822 lines (eof=1, identifier=3827, number=364, string=880, punct=7947)
产出是两套 token 定义中的粗流(Lexeme,见 src/lexeme.zig):
12B(kind + flags + start + end),trivia 不进流(对齐 yuku 等引擎的
交付口径),「前面有换行」压成 1-bit newline_before flag;细流
(Token/TokenTag,src/token.zig)直接采用
yuku 定义,留作 parser 接口预备。
作为库使用(build.zig.zon 依赖 + @import("my_scanner")):
const result = try my_scanner.scan(allocator, src);
for (result.tokens) |lex| { ... }MIT