尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

simdjson:利用 SIMD 指令实现 GB/s 级 JSON 解析的 C++ 开源库

simdjson:利用 SIMD 指令实现 GB/s 级 JSON 解析的 C++ 开源库 适用读者刚入门 C 的开发者、被 JSON 解析速度困扰的后端工程师、一切对每秒能吃掉多少条日志有执念的人。 本文约定所有示例代码基于 simdjsonv3.x向下兼容 v2.x 的 DOM 用法要求 C17 及以上。1. 这库是什么30 秒认识 simdjsonsimdjson是一个用 C 编写的高性能 JSON 解析库核心卖点就一句话别人解析 JSON 的单位是兆字节每秒MB/s它直接干到了吉字节每秒GB/s快了 10~50 倍。为什么能做到因为它在 CPU 层面用上了SIMDSingle Instruction, Multiple Data单指令多数据指令。普通程序处理 JSON 是一个字符一个字符排队过安检而 simdjson 让 CPU一次指令同时检查 32 个字符相当于把单通道安检口换成了 32 通道并行安检口。打个比方传统解析器像人工抄写员一个字母一个字母地抄simdjson 像一台高速扫描仪一页纸唰地一下整页进整页出。注意定位我们之前写过一篇 nlohmann/json 开源 JSON 库 的博客那是通用易用派的代表功能全、写起来爽而 simdjson 是极致性能派的代表快、省内存、只读不写。两者定位不同nlohmann/json 是随身瑞士军刀simdjson 是工业级传送带。本文主角是后者。2. 开源背景作者、协议、Star 量级项目说明项目主页github.com/simdjson/simdjson主要作者Daniel Lemire加拿大魁北克大学教授数据科学/高性能计算领域知名学者核心贡献者还包括 John Keiser、Geoff Langdale 等首次发布2019 年配套论文Parsing gigabytes of JSON per second发表于 VLDB Journal开源协议Apache License 2.0宽松商业友好可自由商用、修改、分发Star 量级20k截至 2026 年长期位列 C 高性能库头部语言要求C17 及以上纯头文件 少量源文件跨平台Windows / Linux / macOS / ARM官方性能官方基准在 4GHz Skylake支持 AVX2上解析 Twitter JSON 数据集可达2.5 GB/s⚠️ Star 数是动态数据写作时以 GitHub 页面实时为准本文给出的量级用于说明这是社区公认的主流高性能库。simdjson 的诞生背景很直接JSON 已经成为互联网事实标准数据格式但传统解析器如 nlohmann/json、RapidJSON 的普通模式在大量场景下只能跑到 100~400 MB/s。对每秒钟要处理几 GB 日志的团队来说解析器就是瓶颈。Daniel Lemire 团队从 2016 年前后开始研究用 SIMD 加速 JSON 结构识别最终产出了这个库。3. 为什么要用四大优点逐个拆解3.1 优点一SIMD 加速原理——让 CPU 一把抓第 1 步理解 SIMD 是什么普通 CPU 指令一次处理一个数据比如一次读 1 个字节。SIMD 指令x86 上的 SSE/AVX2/AVX-512、ARM 上的 NEON允许 CPU一次加载 16/32/64 个字节到同一个大寄存器里然后一条指令同时对它们做运算。类比普通模式是一个邮递员一次送一封信AVX2 模式是一个邮递员骑着三轮车一次送 32 封信。第 2 步JSON 解析的瓶颈在哪里解析 JSON 最耗时的工作不是读数字而是扫描整段文本找结构符号找 { } [ ] : , 这些骨架符号structural character判断哪些引号是字符串的引号、哪些是转义 \ 的一部分避免把字符串里的 : 误判成键值分隔符判断哪些空白可以被跳过。传统解析器逐字节判断if (c { || c } || ...) 一路比下去遇到 1GB 文本就要做几十亿次单字节比较。第 3 步simdjson 怎么用 SIMD 加速simdjson 的核心技巧论文里叫structural character identification分三层一次处理 32/64 字节用 AVX2 一次加载 32 字节用_mm256_cmpeq_epi8逐字节相等比较之类的指令同时判断这 32 个字节里哪些是 {、哪些是 }……所有比较都在一条指令里完成结果是一个 32 位的掩码bitmask每一位对应一个字节是不是某类字符。多路并行比较合并分别得到是花括号的掩码是方括号的掩码是引号的掩码等再用位运算 OR 合并成一张结构符号位图。快速跳过字符串与转义字符串内部用 SIMD 找 和 \一次跳过整个字符串内容而不是逐字符走进字符串。最终效果无论 JSON 里有多少内容识别结构符号的开销从每字节若干条指令降到每 32 字节若干条指令这就是数量级的差距来源。小白类比传统解析是拿着放大镜逐字核对错别字simdjson 是整段文字用 OCR 扫描仪一次过——扫描仪当然也有代价要先凑齐 32 个字节但吞吐量完胜。3.2 优点二GB/s 级性能——快到什么程度官方与第三方基准普遍给出如下量级数据源simdjson 官方 README 及论文Intel Skylake / Apple M1 等现代 CPU解析器大致吞吐4GHz x86说明simdjsonAVX22.5 ~ 3.5 GB/s官方基准 Twitter JSON 数据集RapidJSON普通模式~300 MB/s老牌高性能库SIMD 支持有限nlohmann/json~100 ~ 200 MB/s易用性王者性能一般各语言标准库 JSON通常 500 MB/s含 Python/JS 等解释型实现换算成业务语言1GB 的 JSON 日志simdjson 大约 0.3~0.4 秒解析完而 nlohmann/json 需要 5~10 秒。这就是GB/s 级的含义——不是能解析 GB 大小的文件而是解析速度以 GB/s 计。性能还体现在内存分配极少simdjson 解析过程几乎不产生临时字符串拷贝见下一条大幅减少 malloc/free 和 cache miss。3.3 优点三零拷贝 惰性解析——只看不搬这是 simdjson 和传统 DOM 库最本质的设计差异分两点1) 零拷贝zero-copy视图传统 DOM 解析器如 nlohmann/json会复制数据把 JSON 里的字符串 name: Alice 复制进一个新的 std::string 对象建一棵完全独立于原始文本的对象树。simdjson 不这么做它解析出的 dom::element 只是指向原始缓冲区的视图相当于给原始文本贴了个标签这里是一个字符串、那里是一个数字。取值时它返回 std::string_view——一个看得到这段文本但不拥有它的轻量指针长度。好处解析过程几乎不分配内存取字符串零拷贝直接看原始缓冲区。代价⚠️ 易错点原始 JSON 缓冲区必须活得比解析结果久。如果你把 JSON 字符串局部变量销毁了再去读结果就是悬空引用use-after-free。2) 惰性解析on-demand按需惰性取值v2.0 起引入的 on-demand API 更进一步你取哪个字段它才解析哪个字段而不是把整棵 JSON 树先建好。类比整棵 JSON 是一栋大楼DOM API 是先把整栋楼的房间钥匙全部配好给你on-demand 是你走到哪个房间门口才现场开那把锁。如果你只需要 100 个字段里的 2 个on-demand 能省掉大约 90% 的解析工作量。3.4 优点四易用 API——简单到像读字典虽然底层用 SIMD 很硬核但对外 API 非常友好两行就能解析simdjson::dom::parser parser; // 创建一个解析器可复用 simdjson::dom::element doc parser.parse(json); // 解析 JSON得到根元素 std::string_view name doc[name]; // 像字典一样取值配合 simdjson_result带错误码的结果类型出错时不用捕获异常直接 if (!result) { ... } 判断即可既安全又高效。3.5 与 nlohmann/json 的对比表格维度simdjsonnlohmann/json定位极致性能的只读解析器通用易用、功能全面的 JSON 库解析速度GB/s 级快 10~50 倍100~200 MB/s 量级底层技术SIMDAVX2/AVX-512/NEON 零拷贝视图传统逐字符 内存复制建树字符串取值std::string_view零拷贝std::string复制惰性解析✅ on-demand API 按需解析❌ 必须整体建 DOM 树修改/生成 JSON❌ 只读可 minify不能改树✅ 支持增删改查、序列化输出任意精度/自定义类型❌ 有限✅ 支持自定义类型转换错误处理错误码 simdjson_result可选异常默认抛异常数据所有权要求⚠️ 原始缓冲区须长于结果不要求数据已复制学习成本中需理解视图生命周期低上手极快适用场景高频读、大吞吐、日志/网关/管道通用业务、CRUD、需要写 JSON 的场景结论一句话只读、量大、追求吞吐 → simdjson要写、要改、图省事 → nlohmann/json。两者也可以混用simdjson 读出来转成 nlohmann 对象再往业务层传。4. 典型使用场景哪些项目应该选它场景为什么适合 simdjson典型形态日志分析海量 NDJSON每行一条 JSON持续灌入parse_many 流式处理吞吐决定一切日志采集、ELK 前置解析、告警规则匹配大数据管道ETL每批几 GB JSON 要快速清洗、提取字段、写入下游数据湖入湖、实时数仓、特征提取API 网关 / 反向代理每个请求都要解析 JSON 做路由/鉴权/限流解析时间是关键延迟网关中间件、BFF 层、请求改写实时数据流行情、IoT 遥测、监控指标每秒成千上万条 JSON行情推送、设备上报、指标聚合数据库/缓存层存储引擎内部要把 JSON 当可查询文档读多写少文档型扩展、JSONB 前置解析桌面/嵌入式工具只需快速读取配置或数据文件希望零拷贝省内存配置文件解析、离线分析工具不适合需要频繁修改 JSON 结构、需要把对象重新序列化成 JSON 输出、解析次数很少而开发效率优先的小工具——这些请继续用 nlohmann/json 或 RapidJSON。5. 快速上手从零到跑通分步教程第 1 步准备编译环境编译器支持 C17 的 GCC 7 / Clang 5 / MSVC 2019Windows 建议 VS2019 或更高。CMake3.15。CPU无需手动开启 SIMDsimdjson 默认运行时自动检测runtime dispatch在支持 AVX2/NEON 的机器上自动用最快路径当然想榨干性能也可以编译期指定 -marchnative见第 7 节。⚠️ 不需要手动 #include immintrin.h 或写任何 SIMD 代码库内部已封装好。第 2 步用 CMake 集成 simdjson推荐FetchContent方式一条命令拉取并编译无需提前安装cmake_minimum_required(VERSION 3.15) project(simdjson_demo CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 1. 从 GitHub 拉取 simdjson 源码可换成 tag 或 commit 固定版本 include(FetchContent) FetchContent_Declare( simdjson GIT_REPOSITORY https://github.com/simdjson/simdjson.git GIT_TAG v3.12.0 # 建议固定版本避免上游变动 ) FetchContent_MakeAvailable(simdjson) add_executable(simdjson_demo main.cpp) # 2. 链接 simdjsonCMake 会自动处理编译参数与运行时分发 target_link_libraries(simdjson_demo PRIVATE simdjson)如果你已通过包管理器vcpkg / Conan或系统安装 simdjson也可以用更轻量的方式find_package(simdjson REQUIRED) target_link_libraries(simdjson_demo PRIVATE simdjson::simdjson)第 3 步写第一段解析代码创建 main.cpp#include iostream #include string #include simdjson.h // 只需包含这一个头文件 int main() { // 1. 准备一段 JSON 文本注意simdjson 要求输入是完整合法的 JSON std::string json R({ service: auth, status: ok, latency_ms: 12 }); // 2. 创建解析器可复用不要每解析一次就新建 simdjson::dom::parser parser; // 3. 解析parser.parse 返回 simdjson_result可直接当 bool 判断 simdjson::dom::element doc parser.parse(json); if (doc.error()) { // 出错时 error() 返回错误码对象 std::cerr 解析失败: simdjson::error_message(doc.error()) std::endl; return 1; } // 4. 像字典一样取值operator[] 支持整数下标和字符串键 std::string_view service doc[service]; // 返回 string_view零拷贝 std::string_view status doc[status]; int64_t latency doc[latency_ms]; // 数字可隐式转换为 int64_t // 5. 输出 std::cout service service std::endl; std::cout status status std::endl; std::cout latency latency ms std::endl; return 0; }第 4 步编译并运行# 在 CMake 工程根目录 cmake -B build -DCMAKE_BUILD_TYPERelease cmake --build build --config Release ./build/simdjson_demo预期输出service auth status ok latency 12 ms⚠️必须用 Release 模式Debug 模式-O0下 SIMD 内联优化被关闭性能会退化到和普通库差不多甚至更慢。6. 核心用法详解可运行代码下面每个小节都给出可直接运行的示例。为节约篇幅示例共用同一个解析器声明实际使用时请把代码放进同一个文件或按需合并。6.1 DOM API传统的一次性解析DOMDocument Object Model文档对象模型是先把整棵树建好再随便查的经典模式。适合同一份 JSON 会被反复读取多次的场景。#include iostream #include string #include simdjson.h int main() { std::string json R({ users: [ {id: 1, name: Alice, tags: [admin, c]}, {id: 2, name: Bob, tags: [dev]} ], total: 2 }); simdjson::dom::parser parser; simdjson::dom::element doc parser.parse(json); if (doc.error()) { std::cerr 解析失败: simdjson::error_message(doc.error()) std::endl; return 1; } // 1. 取数组 simdjson::dom::array users doc[users]; // 2. 遍历数组取每个对象的字段 for (simdjson::dom::element user : users) { int64_t id user[id]; std::string_view name user[name]; std::cout id id , name name std::endl; // 3. 嵌套数组 simdjson::dom::array tags user[tags]; for (std::string_view tag : tags) { // 数组元素是字符串可直接转 string_view std::cout tag: tag std::endl; } } // 4. 取根对象的另一个字段 std::cout total doc[total].get_int64() std::endl; return 0; }关键 API 速查表达式含义parser.parse(json)解析 JSON返回根元素doc[key]按字符串键取值arr.at(0) / arr[i]按下标取数组元素element.get_int64() / get_double()显式取数字element.get_string()显式取字符串返回 std::string_viewelement.is_object() / is_array() / is_null()类型判断element[a][b]链式取嵌套字段6.2 on-demand按需惰性取值on-demand按需惰性是 simdjson 官方推荐的默认路径访问到哪个字段才解析哪个字段。适合JSON 很大但我只关心其中几个字段的场景。#include iostream #include string #include simdjson.h int main() { std::string json R({ header: {request_id: abc-123, timestamp: 1723000000}, payload: {data: 这里有一大堆用不到的数据...} }); // on-demand 使用独立的解析器类型 simdjson::ondemand::parser parser; // iterate() 不是立即解析而是创建一个惰性迭代器JSON 数据需存活到读取完成 simdjson::ondemand::document doc parser.iterate(json); if (doc.error()) { std::cerr 解析失败: simdjson::error_message(doc.error()) std::endl; return 1; } // 只取我们关心的字段——其余部分payload根本不会被解析 std::string_view request_id doc[header][request_id]; int64_t ts doc[header][timestamp]; std::cout request_id request_id std::endl; std::cout timestamp ts std::endl; return 0; }⚠️ on-demand 是单遍读取模型同一路径的 doc[header] 不能反复取除非重新 iterate。如果需要反复读请用 DOM API。另外on-demand 的惰性意味着某些错误如字段类型不匹配会在取值那一刻才暴露而不是 parse 时。6.3 流式解析 parse_many一行一条NDJSON日志、事件流最常见的格式是NDJSONNewline-Delimited JSON每行一条 JSON。用 parse_many 可以流式处理不用等整个文件读完#include iostream #include string #include simdjson.h int main() { // 模拟 3 行 NDJSON末尾不要漏换行parse_many 需要靠它切分 std::string ndjson R({level:INFO,msg:started} {level:ERROR,msg:disk full} {level:WARN,msg:retry} ); simdjson::dom::parser parser; simdjson::dom::document_stream docs parser.parse_many(ndjson); // 遍历每一行 JSON for (simdjson::dom::element doc : docs) { if (doc.error()) { std::cerr 某行解析失败: simdjson::error_message(doc.error()) std::endl; continue; } std::string_view level doc[level]; std::string_view msg doc[msg]; std::cout [ level ] msg std::endl; } return 0; }⚠️ parse_many 要求输入以换行符分隔且末尾有换行文件太大时可用 parse_many(json, batch_size) 指定批大小控制内存。6.4 minify把 JSON 压到最小minify 会去掉 JSON 中所有可有可无的空白空格、换行、制表符常用于减少存储和网络传输体积#include iostream #include string #include simdjson.h int main() { std::string pretty_json R({ name : simdjson, stars : 20000 }); // minify 直接原地压缩第二个参数是输出缓冲需预留空间 std::string compact; compact.resize(pretty_json.size()); // 压缩结果不会比原文更长预留即可 size_t written simdjson::minify(pretty_json, compact.data()); compact.resize(written); // 截断到实际写入长度 std::cout 压缩前: pretty_json.size() 字节 std::endl; std::cout 压缩后: compact.size() 字节 std::endl; std::cout compact std::endl; return 0; }⚠️ minify 不会删除字符串内部的空格例如 hello world 里的空格是数据的一部分必须保留。6.5 JSON Pointer按路径精准取值JSON PointerRFC 6901用 / 分隔的路径字符串定位深层字段适合路径来自配置或外部输入的场景#include iostream #include string #include simdjson.h int main() { std::string json R({ api: { v1: { endpoints: [/login, /logout], timeout_ms: 5000 } } }); simdjson::dom::parser parser; simdjson::dom::element doc parser.parse(json); if (doc.error()) return 1; // at_pointer 使用 JSON Pointer 语法/根键/子键/数组下标 std::string_view endpoint doc.at_pointer(/api/v1/endpoints/0); int64_t timeout doc.at_pointer(/api/v1/timeout_ms); std::cout endpoint endpoint std::endl; std::cout timeout timeout ms std::endl; return 0; }等价写法doc[api][v1][endpoints][0]。JSON Pointer 的优势是路径可以动态拼接比如 std::string path /api/ version /endpoints/0。6.6 错误处理不抛异常的代价simdjson 默认不抛异常所有可能失败的操作都返回 simdjson_resultT。检查错误有两种方式#include iostream #include string #include simdjson.h int main() { std::string bad_json R({name: Alice, ); // 故意缺右括号 simdjson::dom::parser parser; // 方式一用 .error() 判断推荐 simdjson::dom::element doc parser.parse(bad_json); if (doc.error()) { std::cout 方式一捕获错误: simdjson::error_message(doc.error()) std::endl; return 0; } // 方式二直接把结果当 bool 用simdjson_result 重载了 operator bool // simdjson::dom::element doc2 parser.parse(bad_json); // if (!doc2) { ... } return 0; }如果你更喜欢异常风格可以在编译时定义 SIMDJSON_EXCEPTIONSON然后直接写 simdjson::dom::element doc parser.parse(json);出错时库会抛 simdjson_error。⚠️ 不要忽略错误返回值解析失败时元素内容是未定义的继续使用会得到垃圾数据。7. 易错点与性能调优清单易错点 / 调优点说明1⚠️原始缓冲区生命周期零拷贝意味着解析结果引用原文本。请确保 JSON 字符串存活到所有取值结束。2⚠️必须 Release 编译Debug 模式关闭优化SIMD 内联失效性能骤降。3⚠️不要复用同一 DOM 元素跨多次 parseparser.parse() 会复用内部缓冲区旧元素视图可能失效需要保留就复制或改用 on-demand 重新 iterate。4⚠️on-demand 是单遍读取同一路径不可重复取需反复读请用 DOM。5⚠️parse_many 末尾要有换行否则最后一条可能解析不出来或报错。6解析器复用一个 parser 反复 parse()比每次 new 一个快得多复用内部缓冲区。7开启 CPU 原生指令集编译加 -marchnativeGCC/Clang或 /arch:AVX2MSVC可让 SIMD 路径固定为最强注意换机器部署时可能不兼容生产建议保留运行时自动检测。8按需取用别整树搬运大 JSON 只取少数字段时用 on-demand全部字段都要且反复读时用 DOM。9优先 std::string_view取值尽量保留 string_view不要立刻转 std::string会触发拷贝。10大文件分批超大文件用 parse_many(json, batch_size) 或 mmap 后分段解析控制峰值内存。8. FAQ 速查表问题速答simdjson 要装什么依赖零第三方依赖只有 C17 编译器 CMake。我需要在代码里写 SIMD 吗不需要库内部自动处理默认运行时自动检测指令集。为什么比我手写的解析器快这么多一次处理 32/64 字节 零拷贝视图 惰性解析三层优化叠加。simdjson 能修改 JSON 吗不能修改 DOM 树只能读取、minify。要修改请用 nlohmann/json。和 nlohmann/json 怎么选只读大吞吐 → simdjson读写都要/图省事 → nlohmann/json。解析结果能保存到下次再用吗DOM 元素跨 parse 会失效长期保存请复制数据或转成自己的结构。on-demand 和 DOM 有什么区别on-demand 用到才解析快、省内存、单遍DOM 一次建全树可反复读。支持 JSON 里的大数字超过 int64吗数字以 int64/double 为主超大整数需自己按字符串取get_raw_json_number。支持 Windows 吗支持 MSVC 2019同样自动选择 SSE4/AVX2 路径。出错会崩吗不会默认返回错误码可选开启异常模式。9. 参考资料simdjson 官方仓库与文档GitHub - simdjson/simdjson: Parsing gigabytes of JSON per second : used by Facebook/Meta Velox, the Node.js runtime, ClickHouse, WatermelonDB, Apache Doris, Milvus, StarRocks · GitHub含 API 文档、性能报告G. Langdale, D. Lemire.Parsing gigabytes of JSON per second.VLDB Journal, 2019.RFC 8259JSON 数据交换格式、RFC 6901JSON Pointer相关阅读本站已发布的 C 主题nlohmann/json 开源 JSON 库、Google Benchmark 开源基准测试库
返回列表