
flashjson:为 CPython 从零打造的双后端 JSON 解析器
来源标题:Show HN: flashjson
开发者发布 flashjson,一个从零实现的 CPython JSON 解析器,提供 C 扩展和纯 Python 两个后端。C 后端采用 Eisel-Lemire 浮点解析和 simdutf 转码,纯 Python 后端确保无编译器环境也能导入。其错误类型和关键字与标准库 json 保持一致,替换解析器无需改代码。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
为 CPython 从零实现的 JSON 解析器,提供两个后端:一个 CPython C 扩展用于速度,一个手工调优的纯 Python 模块,使得该包在没有编译器的机器上仍可导入。 import flashjson # pip install flashjson obj = flashjson.loads(text) # 与 json.loads 相同的语义和关键字 obj = flashjson.loads(b'{"a":1}')# bytes / bytearray / memoryview 也行 支持的关键字:parse_float、parse_int、parse_constant、object_hook、object_pairs_hook。错误是标准库的 json.JSONDecodeError,消息也来自标准库,所以 except json.JSONDecodeError 继续有效,换解析器时错误文本不变。 c/flashjson.c CPython C 扩展(快速路径) c/lemire_float.h Eisel-Lemire 浮点解析(来自 simdjson,Apache-2.0) c/simdutf/ 内置的 simdutf amalgamation(Apache-2.0 / MIT)—— 通过 c/simdutf_shim.cpp 进行 SIMD UTF-8 → UCS-2/UCS-4 转码 flashjson/init.py 存在 C 扩展时选用它,否则用回退 flashjson/_py.py 纯 Python 回退(无需编译器) tests/ 与标准库 json 的对等性 + fuzz 测试 bench.py 与标准库 json(以及已安装的 orjson)的基准测试 pgo_train.py 可选 PGO 构建的训练语料 安装 pip install flashjson # wheel:见 Releases 页面 / PyPI 纯 Python 后端保持完全相同的语义且无需编译器,因此 pip install 在任何地方都能用。 Wheel 由 CI 从源码分发包构建,面向 CPython 3.14,覆盖 linux x86_64/aarch64、macOS arm64/x86_64 和 Windows AMD64。发布构建以 x86-64 基线为目标;内置的 simdutf 在运行时选择 SIMD 内核,所以一个 wheel 覆盖整个架构。 从源码构建(不用 pip,除 C 和 C++ 编译器外无需构建依赖): ./build.sh # -> flashjson/_c<ext>.so,可移植基线 NATIVE=1 ./build.sh # -march=native,快约 3-6 %,仅限本机 PGO=1 ./build.sh # 配置文件引导,在此基础上再快约 3-5 % python tests/test_flashjson.py # 与标准库 json 对等,fuzz 测试 python bench.py # 对比标准库 json(以及已安装的 orjson) 或者用 pip 针对一份 checkout: pip install . # scikit-build-core + CMake 性能 2026-11 在一台 x86-64 台式机上测量,使用 wheel 构建时的可移植配置(-march=x86-64-v2,PGO=1),绑定到单个核心,取 N 次最小值,对比 orjson 3.13.0。单位 MB/s,越高越好: 最高达标准库 json 模块的 5.1 倍,在 7 个数据集中的 6 个上快于 orjson——小文档、bytes 输入、浮点密集的几何数据、unicode 文本和混合流量上快 1.1–1.34 倍,深度嵌套重复上快 1.02 倍。重复值文档正是缓存发光的地方:反复出现的短 ASCII 字符串达到 orjson 的 1.4–1.7 倍。 orjson 唯一保持几个百分点优势的数据集是紧凑、键密集的 1 MB 流量(0.95 倍),其余各处两个解析器都在 5 % 以内。 docs/BENCHMARKS.md 有按形态的测量、剩余周期去向的消融分析,以及我们测量并否决的优化列表。 自己运行:python bench.py(标准库 json,以及已安装的 orjson)。 直接解析 str 的 UTF-8 缓冲区(PEP 393)。 PyUnicode_AsUTF8AndSize 在对象上缓存,所以重复解析同一个 str 是免费的。输入中的孤立代理项通过 WTF-8 + surrogatepass 处理,与标准库一致。 融合 SIMD 字节扫描器:一趟找到 { '"', '\', < 0x20 } 中的第一个字节并跟踪 ASCII 性,32 字节/步(AVX2)或 16 字节/步(SSE2——x86-64 基线,无需运行时分发),然后是 8 字节 SWAR 尾部(haszero/hasless),最后是标量——由 _c._selftest() 与朴素循环逐字节核对。 每个字符串一趟融合处理,端到端:终止符搜索、控制字符校验和 ASCII 检测在一次扫描中完成;转义在转义之间用分块 memcpy 解码。干净的字符串是 PyUnicode_New + 一次 memcpy。 非 ASCII 字符串(≥ 32 B)通过内置的 simdutf 进行 SIMD UTF-8 → UCS-2/UCS-4 转码;短字符串使用我们的标量解码器加 SSE2 ASCII 串加宽。对象用 PyUnicode_FromKindAndData(完全 finalize)构建,CPython 的错误处理器机制只在病态输入时才会触及——这就是为什么 unicode-strings 这一行胜过 orjson,尽管 orjson 有自己的解码器。 带自适应准入策略的持久字符串缓存:重复的短 ASCII 字典键和短 ASCII 字符串值命中开放寻址表并完全跳过 str 构造(值不可变,所以共享是安全的)。值插入在预算上运行,命中则补充、未命中则消耗:值全不相同的文档不再为只会被逐出的插入付出代价(实测:唯一字符串文档 +22 %,命中的文档无损失)。 数字:token 用 SWAR 数字串扫描定位,有效数字每次 8 个(SWAR)转换为 u64——没有字符串往返,也没有串行的 m*10+d 延迟链。浮点数先走可证明正确的 Clinger 快速路径(≤ 15 位数字,|e| ≤ 22),然后 Eisel-Lemire(19 位数字,全指数范围),只有 19 位数字的病态输入才用 strtod。每条路径都与 Python 的 float() 位级一致(在 40 万个值上验证,包括精确的 round-half-even 平局)。 PyUnicode_New(len, maxchar) 不会让新构造的非 ASCII 字符串处于 finalized 状态:把 UCS-2 数据原始 memcpy 进 PyUnicode_New(len, 0xFFFF) 得到的字符串 repr 和迭代都正确,但哈希和相等性已损坏。要通过 PyUnicode_FromKindAndData 写入(或对 ASCII 用 PyUnicode_New(len, 127) + memcpy,CPython 会立即 finalize)。BMP 快速路径正是因为这个原因使用 PyUnicode_FromKindAndData。 在所有测试加上约 1.1 万个 fuzz/变异文档上与标准库的接受/拒绝一致(重复键后者胜、NaN/Infinity、孤立代理项、/、\uXXXX 对、大整数、-0.0、bytes 输入、钩子……)。 重复的短 ASCII 字符串(键和值)可能在已解析文档之间共享(is 可能跨 loads() 调用成立,如同 interned 字符串)。相等语义不受影响;哪些字符串被共享是
(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。