ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

RapidJSON 常见问题深度解析:DOM/SAX 选型、解析标志位、编码处理与性能内幕

RapidJSON 常见问题深度解析:DOM/SAX 选型、解析标志位、编码处理与性能内幕 RapidJSON 常见问题深度解析DOM/SAX 选型、解析标志位、编码处理与性能内幕【免费下载链接】rapidjsonA fast JSON parser/generator for C with both SAX/DOM style API项目地址: https://gitcode.com/GitHub_Trending/ra/rapidjson本文为 RapidJSONC 高性能 JSON 解析/生成库官方 FAQ 的深度技术导读覆盖从DOM 还是 SAX为何不只用 double 表示数字等 API 设计决策到kParseValidateEncodingFlag校验标志、SIMD 宏开关、ParseResult错误结构等底层实现细节。读完后你将能够准确回答日常使用 RapidJSON 时几乎必然遇到的所有问题并能对照仓库源码验证每一项结论。一、一般问题库的定位与工程事实RapidJSON 是什么为什么叫这个名字RapidJSON 是一个 C 库用于解析及生成 JSON其完整特点可参考 features 文档。它的灵感来自 RapidXML一个高速 XML DOM 解析器并借鉴了其两个核心设计原位in situ解析与只有头文件header-only的库形态。但两者的 API 完全不同且 RapidJSON 提供许多 RapidXML 没有的特性。关于许可与体积仓库中的事实如下RapidJSON 在MIT 协议下免费可用于商业软件详见 license.txt它只依赖 C 标准库因此体积极小——官方 FAQ 给出的参考数据是在 Windows 上一个解析 JSON 并打印统计信息的可执行文件少于 30KB它是 header-only 库include/rapidjson/下的全部头文件即可投入使用。如何安装 RapidJSON按 readme.zh-cn.md 中安装一节的步骤把include/rapidjson目录复制到系统或项目的 include 目录即可开始使用header-only无需编译库本体若要构建测试与示例需要 CMakeLists.txt 声明的构建体系先执行git submodule update --init获取 thirdparty/gtest 子模块创建build目录并执行cmake ..Windows 可用 cmake-gui随后在 Linux 下make在 Windows 下编译 solution生成的测试与示例可执行文件位于bin目录执行测试可用make test或ctestctest -V查看详细输出也可用make install安装到系统其他 CMake 项目通过find_package(RapidJSON)集成。平台兼容性、C 标准与实际生产使用能否运行于我的平台社区已在多个操作系统/编译器/CPU 架构组合上测试过 RapidJSON但官方不保证特定平台一定可行——只需要生成并执行单元测试就能得到答案。单元测试位于 test/unittest/由 test/unittest/CMakeLists.txt 组织C 标准支持RapidJSON 最初按 C03 实现后来加入了可选的 C11 特性如转移构造函数、noexcept应兼容所有符合 C03 或 C11 的编译器是否用于实际应用是的。它被部署在前台与后台真实应用中有社区成员反馈其系统每天解析 5 千万个 JSON如何被测试仓库自带一套单元测试做自动测试TravisLinux与 AppVeyorWindows对应仓库中的 appveyor.yml对所有修改进行编译并执行单元测试Linux 下还会用 Valgrind 检测内存泄漏配套抑制文件为 test/valgrind.supp替代品存在许多开源 C/C JSON 库社区维护的 nativejson-benchmark 项目对众多库做过横向评测json.org 也维护有一份列表此处不展开外部链接。二、JSON 相关标准符合性什么是 JSONRapidJSON 符合标准吗JSONJavaScript Object Notation是一种轻量级数据交换格式使用人类可读的文本形式常用于 Web 应用传递结构化数据也可作为文件格式用于数据持久化。规范细节见 RFC 7159 与 ECMA-404 标准。RapidJSON完全符合RFC 7159 及 ECMA-404并额外处理若干特殊情况支持 JSON 字符串中包含空字符\u0000支持UTF-16 代理对surrogate pair的解析与生成详见后文 Unicode 一节。是否支持宽松宽松语法JSON目前不支持。RapidJSON 只支持严格的标准 JSON 语法——单引号、无引号键、尾随逗号、注释等扩展语法都会被解析器拒绝。宽松语法需求在社区的 issue 中讨论但未成为正式特性。三、DOM 与 SAX两种 API 的取舍什么是 DOM 风格与 SAX 风格 APIDOMDocument Object ModelJSON 在内存中的完整表示便于查询与修改例如遍历、取子节点、改值SAX事件驱动的 API解析器/生成器在流式过程中回调StartObject()、Key()、String()、EndArray()等事件速度极快且省内存但编程通常更困难。选择原则很直接需要事后查询、修改就用 DOM追求极致的速度和内存、只做单向流式处理就用 SAX。什么是原位in situ解析原位解析把 JSON 直接解码到输入的 JSON 字符串缓冲区内部而不是另建一套字符串对象。这是一种以允许修改输入缓冲区换取更低内存消耗和更高性能的优化。更多细节见 DOM 文档。什么时候会产生解析错误错误信息如何表达出现以下三种情况之一时解析器产生错误输入 JSON 含非法语法输入不能表示为某个值例如数字超出了可表示范围解析器或处理器中断了解析过程。错误信息统一存储在ParseResult中。从源码看error/error.h它只含两个核心成员struct ParseResult { ParseResult() : code_(kParseErrorNone), offset_(0) {} ParseResult(ParseErrorCode code, size_t offset) : code_(code), offset_(offset) {} ParseErrorCode Code() const; // 错误代号 size_t Offset() const; // 从 JSON 开始到错误处的字符数 bool IsError() const; // 是否为错误 // 还支持显式转换为 bool可直接写 if (!doc.Parse(...)) };因此拿到ParseResult后可以调用Code()得到错误代号、Offset()得到出错偏移再借助 error/en.h 中的GetParseError_En()把代号翻译为人类可读的错误消息。值得注意的还有一个解析标志位kParseIterativeFlag迭代式解析定义在 reader.hkParseValidateEncodingFlag 2, // 校验 JSON 字符串的编码 kParseIterativeFlag 4, // 迭代式解析函数调用栈开销为常数复杂度对于嵌套非常深的 JSON可推断传统递归式解析会受函数调用栈深度限制kParseIterativeFlag通过迭代式状态机见 iterative-parser-states-diagram避免了这一限制。为何不只用 double 表示 JSON number一些应用需要 64 位有符号/无符号整数而这些整数不能无损转换成doubledouble的尾数只有 53 位有效位。因此 RapidJSON 的解析器会检测每个 JSON number 能安全转换成哪些整数类型以及double并在Value内部以最小的类型保存从而保证如GetInt64()能返回精确值。如何清空并最小化 document/value 的容量调用SetXXX()系列方法——它们会先调用析构函数释放原有内容再原地重建一个空的 Object 或 Array。以SetObject()为例源码实现document.h就是一行析构 placement newGenericValue SetObject() { this-~GenericValue(); new (this) GenericValue(kObjectType); return *this; }用法Document d; // ... d 已解析出很大的对象 d.SetObject(); // clear and minimize等价的两种写法C swap with temporary idiomValue(kObjectType).Swap(d); // 或 d.Swap(Value(kObjectType).Move());如何把一个 document 节点插入到另一个 document设有两个 DOMDocument person; person.Parse({\person\:{\name\:{\first\:\Adam\,\last\:\Thomas\}}}); Document address; address.Parse({\address\:{\city\:\Moscow\,\street\:\Quiet\}});希望把整个address作为子节点插入person得到{ person: { name: { first: Adam, last: Thomas }, address: { city: Moscow, street: Quiet } } }关键约束DOM 中的Value使用转移语义节点内存由创建它的 allocator 管理。若直接把address的节点挂到person上一旦address析构节点内存就被释放person中会留下悬垂引用。因此必须保证节点的生命周期与目标文档一致有三种做法做法一让 address 直接使用 person 的 allocatorDocument address(person.GetAllocator()); // 注意使用 person 的分配器 address.Parse({\address\:{\city\:\Moscow\,\street\:\Quiet\}}); person[person].AddMember(address, address[address], person.GetAllocator());不想硬编码 key 名时可用迭代器auto addressRoot address.MemberBegin(); person[person].AddMember(addressRoot-name, addressRoot-value, person.GetAllocator());做法二深拷贝后插入Value addressValue Value(address[address], person.GetAllocator()); // 深拷贝 person[person].AddMember(address, addressValue, person.GetAllocator());原文 FAQ 中的Documnet为笔误正确类名为Document。四、Document/ValueDOM设计决策 FAQ什么是转移语义为什么要这样设计Value不用复制语义而用转移语义把来源值赋值给目标值时来源值的所有权直接转移到目标值来源被掏空。由于转移一次指针交换远快于复制整棵子树深拷贝这个设计强迫使用者意识到复制是有消耗的操作避免无意中发生昂贵的深拷贝。怎样复制一个值有两个官方 API带 allocator 的构造函数Value(const Value rhs, Allocator allocator)CopyFrom()成员函数源码见 document.hGenericValue CopyFrom(const GenericValue rhs, Allocator allocator, bool copyConstStrings false);用例可参考 tutorial 文档 中深复制 Value一节。为什么 API 要求提供字符串长度C 字符串以空字符结尾取长度需要strlen()——这是线性复杂度操作在已知长度时调用它是白白浪费更关键的是RapidJSON 支持包含\u0000空字符的 JSON 字符串。此时strlen()返回的长度不等于真实字符串长度使用者必须显式提供长度。为什么许多 DOM 操作 API 要传 allocator 参数因为它们是Value的成员函数而 RapidJSON不希望为每个Value都存一个 allocator 指针——那样会显著增大每个Value的内存占用性能一章会看到Value只有 16/24 字节。把 allocator 作为调用参数传入是典型的空间换 API 冗余度决策。它会转换各种数值类型吗会但规则严格GetInt()、GetUint()、GetInt64()等 API 可能触发整数到整数的转换——仅当保证转换安全时才转换否则断言失败RapidJSON 以断言而非异常报告这类编程错误把 64 位有符号/无符号整数转换为double时会转换但可能损失精度含小数的数字、或超出 64 位的整数只能用GetDouble()获取。五、Reader/WriterSAX为什么不能直接 printf为什么不用 printf 直接输出 JSON而需要 Writer官方给出的理由有四条每一条都指向printf的真实缺陷格式正确性Writer保证输出是合法 JSON。错误地调用 SAX 事件序列如StartObject()之后配EndArray()会触发断言失败从而在开发期暴露 bug字符串转义Writer会自动对字符串做转义如\n、引号、非 ASCII 可配转义手写 printf 极易漏转义locale 陷阱printf()的数值输出受 locale 影响某些区域设置会插入数字分隔符产出非法的 JSON number性能Writer的数值转字符串使用了高度优化的算法dtoa/itoa见 include/rapidjson/internal/dtoa.h 与 itoa.h速度快于printf()与iostream。Writer实现位于 writer.h其WriteDouble路径会优先使用dtoa_算法这正是特别算法的具体落点之一。能否暂停解析、稍后继续基于性能考虑当前版本不直接支持暂停/恢复解析。若执行环境支持多线程一个变通方案是在另一个线程解析 JSON通过阻塞输入流来实现暂停效果——流被Reader逐块读取流不前进解析自然停顿。六、Unicode编码、校验与代理对支持哪些编码能检测编码合法性吗RapidJSON 完全支持UTF-8、UTF-16大端/小端、UTF-32大端/小端及 ASCII。编码合法性校验是可选的把kParseValidateEncodingFlag传给Parse()即可。从源码看该标志在 reader.h 的字符串解析路径中被检查——未开启校验时直接跳过 UTF 合法性判断更快开启后一旦发现非法编码即返回kParseErrorStringInvalidEncoding错误其定义见 error/error.h对应消息为 Invalid encoding in string.error/en.h。什么是代理对surrogate pairJSON 使用 UTF-16 编码转义 Unicode 字符例如\u5927表示大。处理基本多文种平面BMP之外的字符时UTF-16 会将其编码为两个 16 位值称为代理对。例如表情字符 U1F602 在 JSON 中编码为\uD83D\uDE02。RapidJSON完全支持解析及生成 UTF-16 代理对。能处理 JSON 字符串中的 \u0000 吗能完全支持。但使用者必须意识到字符串内部可以含空字符并使用GetStringLength()及相关 API 获取真实长度而不能依赖strlen()理由见为什么需要字符串长度一节。能否把所有非 ASCII 字符输出为 \uxxxx可以。只要Writer的输出编码参数使用ASCII即可强制把所有非 ASCII 字符转义为\uxxxx形式WriterASCII, StringStream writer(str); // 输出中非 ASCII 一律 \u 转义七、流Stream大文件、网络流与自动编码很大的 JSON 文件要整个载入内存吗不必。可以使用FileReadStreamfilereadstream.h逐块buffered读入文件内存占用与文件大小无关。但有一个例外原位解析必须先把整个文件载入内存因为它直接改写输入缓冲区。能解析网络上串流进来的 JSON 吗可以。流的抽象接口非常小Read()/Peek()/Tell()等见 stream.h可以参照FileReadStream的实现写一个网络流数据到达一块就喂给Reader天然支持断点续传式的流式处理。不知道输入是哪种编码怎么办使用AutoUTFInputStream它会自动检测输入流的编码。源码位于 encodedstream.htemplatetypename InputByteStream, typename Encoding class AutoUTFInputStream { // 构造时指定期望的 UTFType若流带 BOM 则自动按 BOM 纠正 AutoUTFInputStream(InputByteStream is, UTFType type kUTF8) : is_(is), type_(type), hasBOM_(false) { ... } };注意自动检测会带来一些性能开销已知编码时应直接用对应的EncodedInputStream。对称地输出侧有AutoUTFOutputStream同文件 encodedstream.h。什么是 BOMRapidJSON 怎么处理字节顺序标记BOM, byte order mark有时出现在文件/流开头用于标示其 UTF 编码类型。RapidJSON 的处理策略EncodedInputStream可以检测并跳过BOMEncodedOutputStream可以选择是否写入BOM。示例见 编码流文档。为什么涉及大端/小端只有UTF-16 与 UTF-32的流需要关心字节序UTF-8 不需要UTF-8 的编码本身可自识别字节序。因此 RapidJSON 提供UTF16LE/UTF16BE、UTF32LE/UTF32BE等编码类型区分端序。八、性能为什么快代价是什么RapidJSON 真的快吗为什么快是——官方 FAQ 的表述是它可能是最快的开源 JSON 库社区的 nativejson-benchmark 项目对多个 C/C JSON 库做过横向评测。快的来源分两层面向时间/空间的设计决策这些决策有时以牺牲 API 易用性为代价例如转移语义、显式字符串长度、显式 allocator 参数底层优化SIMD 指令、C 内部函数intrinsic以及特殊算法——自研的 double 转字符串dtoa与字符串转 doublestrtod见 include/rapidjson/internal/strtod.h。SIMD 是怎么用的如何开启SIMD 指令允许现代 CPU 并行运算。RapidJSON 支持用Intel SSE2/SSE4.2和ARM Neon加速对空白符、制表符、回车符、换行符的过滤在解析带缩进的 JSON 时提升明显。开启方式源码见 rapidjson.h#define RAPIDJSON_SSE2 // 启用 SSE2 优化 #define RAPIDJSON_SSE42 // 启用 SSE4.2 优化 #define RAPIDJSON_NEON // 启用 ARM Neon 优化三个宏任选定义了任意一个后 RapidJSON 会派生定义RAPIDJSON_SIMD宏若同时定义RAPIDJSON_SSE42与RAPIDJSON_SSE2SSE4.2 优先。Reader/Writer内部据此选择优化路径reader.h 顶部按RAPIDJSON_SSE42 → RAPIDJSON_SSE2 → RAPIDJSON_NEON顺序选择实现。重要限制若在不支持相应指令集的机器上运行这些可执行文件会导致崩溃。SIMD 优化测试见 test/unittest/simdtest.cpp。内存消耗如何RapidJSON 的设计目标之一是降低内存占用SAX APIReader消耗内存与JSON 树深度 最长 JSON 字符串成正比与文档总大小无关DOM API每个Value在 32 位架构下约消耗16 字节、64 位架构下约24 字节见 document.h 中的GenericValue定义字符串内容则由 allocator 另行分配RapidJSON 还使用特殊的内存分配器allocators.h 中的CrtAllocator/MemoryPoolAllocator等来减少分配元数据开销其中池分配器为 DOM 场景专门优化。高性能的意义某些应用要处理非常大的 JSON 文件某些后台服务要处理海量 JSON——高性能直接改善延时与吞吐量更广义地说更高的算力利用率也意味着更低的能耗。性能测试入口见 test/perftest/。九、项目背后的故事FAQ 八卦节选原作者叶劲峰Milo Yip2011 年以兴趣项目的身份开启 RapidJSON。作为一名游戏程序员他希望拥有一个快速、只有头文件的 JSON 库。关键贡献Philipp A. Hartmannpah实现大量改进并搭建自动化测试丁欧南thebusytypist实现了迭代式解析器即上文kParseIterativeFlagAndrii Senkovychjollyroger完成向 CMake 的迁移Kosta 贡献了短字符串优化。迁移 GitHubGoogle Code 时代的项目最终迁入 GitHub——大势所趋且 GitHub 更强大方便。十、快速索引本文覆盖的 FAQ 全清单FAQ 主题关键源码/文档佐证安装、C03/11、平台兼容性readme.zh-cn.md、test/unittest/解析错误、ParseResult、错误偏移error/error.h#L106-L129、error/en.hkParseValidateEncodingFlag/kParseIterativeFlagreader.h#L149-L150原位解析doc/dom.zh-cn.md、insituparsing 示意图SetObject()/Swap清空最小化document.h#L1188跨 Document 插入节点document.h 的 allocator 与AddMember深复制构造函数/CopyFrom()document.h#L978、doc/tutorial.zh-cn.mdWriter数值转字符串算法writer.h、internal/dtoa.hAutoUTFInputStream/BOM 处理encodedstream.h#L135、doc/stream.zh-cn.mdSIMD 宏与优先级rapidjson.h#L354-L385、simdtest.cpp特殊分配器allocators.h以上各节均可直接在仓库中检索对应文件进一步深入全部结论以当前仓库源码与文档为准。【免费下载链接】rapidjsonA fast JSON parser/generator for C with both SAX/DOM style API项目地址: https://gitcode.com/GitHub_Trending/ra/rapidjson创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进