适用读者:刚入门 C++ 的开发者、被 JSON 解析速度困扰的后端工程师、一切对"每秒能吃掉多少条日志"有执念的人。 本文约定:所有示例代码基于 simdjson v3.x(向下兼容 v2.x 的 DOM 用法),要求 C++17 及以上。
1. 这库是什么:30 秒认识 simdjson
simdjson 是一个用 C++ 编写的高性能 JSON 解析库,核心卖点就一句话:
别人解析 JSON 的单位是"兆字节每秒(MB/s)",它直接干到了"吉字节每秒(GB/s)",快了 10~50 倍。
为什么能做到?因为它在 CPU 层面用上了 SIMD(Single 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++ 高性能库头部) |
| 语言要求 | C++17 及以上,纯头文件 + 少量源文件,跨平台(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 最耗时的工作,不是"读数字",而是扫描整段文本找结构符号:
传统解析器逐字节判断,if (c == '{' || c == '}' || …) 一路比下去,遇到 1GB 文本就要做几十亿次单字节比较。
第 3 步:simdjson 怎么用 SIMD 加速
simdjson 的核心技巧(论文里叫 structural character identification)分三层:
最终效果:无论 JSON 里有多少内容,识别结构符号的开销从"每字节若干条指令"降到"每 32 字节若干条指令",这就是数量级的差距来源。
小白类比:传统解析是"拿着放大镜逐字核对错别字",simdjson 是"整段文字用 OCR 扫描仪一次过"——扫描仪当然也有代价(要先凑齐 32 个字节),但吞吐量完胜。
3.2 优点二:GB/s 级性能——快到什么程度
官方与第三方基准普遍给出如下量级(数据源:simdjson 官方 README 及论文,Intel Skylake / Apple M1 等现代 CPU):
| simdjson(AVX2) | 2.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 的对比表格
| 定位 | 极致性能的只读解析器 | 通用易用、功能全面的 JSON 库 |
| 解析速度 | GB/s 级(快 10~50 倍) | 100~200 MB/s 量级 |
| 底层技术 | SIMD(AVX2/AVX-512/NEON)+ 零拷贝视图 | 传统逐字符 + 内存复制建树 |
| 字符串取值 | std::string_view(零拷贝) | std::string(复制) |
| 惰性解析 | ✅ on-demand API 按需解析 | ❌ 必须整体建 DOM 树 |
| 修改/生成 JSON | ❌ 只读(可 minify,不能改树) | ✅ 支持增删改查、序列化输出 |
| 任意精度/自定义类型 | ❌ 有限 | ✅ 支持自定义类型转换 |
| 错误处理 | 错误码 simdjson_result(可选异常) | 默认抛异常 |
| 数据所有权要求 | ⚠️ 原始缓冲区须长于结果 | 不要求(数据已复制) |
| 学习成本 | 中(需理解视图生命周期) | 低(上手极快) |
| 适用场景 | 高频读、大吞吐、日志/网关/管道 | 通用业务、CRUD、需要写 JSON 的场景 |
结论一句话:只读、量大、追求吞吐 → simdjson;要写、要改、图省事 → nlohmann/json。两者也可以混用:simdjson 读出来,转成 nlohmann 对象再往业务层传。
4. 典型使用场景:哪些项目应该选它
| 日志分析 | 海量 NDJSON(每行一条 JSON)持续灌入,parse_many 流式处理,吞吐决定一切 | 日志采集、ELK 前置解析、告警规则匹配 |
| 大数据管道(ETL) | 每批几 GB JSON 要快速清洗、提取字段、写入下游 | 数据湖入湖、实时数仓、特征提取 |
| API 网关 / 反向代理 | 每个请求都要解析 JSON 做路由/鉴权/限流,解析时间是关键延迟 | 网关中间件、BFF 层、请求改写 |
| 实时数据流 | 行情、IoT 遥测、监控指标,每秒成千上万条 JSON | 行情推送、设备上报、指标聚合 |
| 数据库/缓存层 | 存储引擎内部要把 JSON 当"可查询文档",读多写少 | 文档型扩展、JSONB 前置解析 |
| 桌面/嵌入式工具 | 只需快速读取配置或数据文件,希望零拷贝省内存 | 配置文件解析、离线分析工具 |
不适合:需要频繁修改 JSON 结构、需要把对象重新序列化成 JSON 输出、解析次数很少而开发效率优先的小工具——这些请继续用 nlohmann/json 或 RapidJSON。
5. 快速上手:从零到跑通(分步教程)
第 1 步:准备编译环境
- 编译器:支持 C++17 的 GCC 7+ / Clang 5+ / MSVC 2019+(Windows 建议 VS2019 或更高)。
- CMake:3.15+。
- CPU:无需手动开启 SIMD!simdjson 默认运行时自动检测(runtime dispatch),在支持 AVX2/NEON 的机器上自动用最快路径;当然,想榨干性能也可以编译期指定 -march=native(见第 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. 链接 simdjson(CMake 会自动处理编译参数与运行时分发)
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_TYPE=Release
cmake –build build –config Release
./build/simdjson_demo
预期输出:
service = auth
status = ok
latency = 12 ms
⚠️ 必须用 Release 模式!Debug 模式(-O0)下 SIMD 内联优化被关闭,性能会退化到和普通库差不多,甚至更慢。
6. 核心用法详解(可运行代码)
下面每个小节都给出可直接运行的示例。为节约篇幅,示例共用同一个解析器声明,实际使用时请把代码放进同一个文件或按需合并。
6.1 DOM API:传统的一次性解析
DOM(Document 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_view) |
| element.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)
日志、事件流最常见的格式是 NDJSON(Newline-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 Pointer(RFC 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_result<T>。检查错误有两种方式:
#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_EXCEPTIONS=ON,然后直接写 simdjson::dom::element doc = parser.parse(json);,出错时库会抛 simdjson_error。
⚠️ 不要忽略错误返回值!解析失败时元素内容是未定义的,继续使用会得到垃圾数据。
7. 易错点与性能调优清单
| 1 | ⚠️ 原始缓冲区生命周期 | 零拷贝意味着解析结果引用原文本。请确保 JSON 字符串存活到所有取值结束。 |
| 2 | ⚠️ 必须 Release 编译 | Debug 模式关闭优化,SIMD 内联失效,性能骤降。 |
| 3 | ⚠️ 不要复用同一 DOM 元素跨多次 parse | parser.parse() 会复用内部缓冲区,旧元素视图可能失效;需要保留就复制或改用 on-demand 重新 iterate。 |
| 4 | ⚠️ on-demand 是单遍读取 | 同一路径不可重复取;需反复读请用 DOM。 |
| 5 | ⚠️ parse_many 末尾要有换行 | 否则最后一条可能解析不出来或报错。 |
| 6 | 解析器复用 | 一个 parser 反复 parse(),比每次 new 一个快得多(复用内部缓冲区)。 |
| 7 | 开启 CPU 原生指令集 | 编译加 -march=native(GCC/Clang)或 /arch:AVX2(MSVC)可让 SIMD 路径固定为最强;注意换机器部署时可能不兼容,生产建议保留运行时自动检测。 |
| 8 | 按需取用,别整树搬运 | 大 JSON 只取少数字段时用 on-demand;全部字段都要且反复读时用 DOM。 |
| 9 | 优先 std::string_view | 取值尽量保留 string_view,不要立刻转 std::string(会触发拷贝)。 |
| 10 | 大文件分批 | 超大文件用 parse_many(json, batch_size) 或 mmap 后分段解析,控制峰值内存。 |
8. FAQ 速查表
| simdjson 要装什么依赖? | 零第三方依赖,只有 C++17 编译器 + 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 8259(JSON 数据交换格式)、RFC 6901(JSON Pointer)
- 相关阅读(本站已发布的 C++ 主题):nlohmann/json 开源 JSON 库、Google Benchmark 开源基准测试库
网硕互联帮助中心




评论前必须登录!
注册