资讯详情

资讯详情

免重建改JSON:REDox可变Token DOM的Version机制设计哲学

免重建改JSON:REDox可变Token DOM的Version机制设计哲学【免费下载链接】REDoxHigh-performance, token-based structured data engine for .NET. A core component of REX, the technology behind CAPCOMs next-generation game engine.项目地址: https://gitcode.com/gh_mirrors/redox/REDox在 .NET 的 JSON 处理生态里一直横着一条看不见的裂痕System.Text.Json的JsonDocument解析快、但它是一卷只读的磁带JsonNode能改、却要把整个文档对象化负担沉重Newtonsoft.Json灵活、但性能与分配量在游戏引擎的量级下并不理想。REDoxRE:Dox作为卡普空下一代游戏引擎 REX 的核心结构化数据引擎试图同时拿到三者的长处tape DOM 的解析速度、node DOM 的可编辑性、以及格式无关的灵活性。这个目标的落点是仓库 README 里那句轻描淡写的话——Mutable token DOM编辑对象与数组时不必用一棵重量级托管对象树重建整个文档。本文从源码出发拆解 REDox 可变 Token DOM 的实现哲学重点回答三个问题Token DOM 凭什么能免重建地改 JSONDValue/DElement句柄上那一套 Version 版本机制如何拦截脏读JSON5 注释保留这类细节背后又藏着怎样的取舍一、Token DOM 的原地修改模型把磁带变成可重链接的索引表传统 tape DOM 之所以只读是因为它的逻辑结构就藏在一段扁平 token 序列里数组的第 N 个元素就是第 N 个 token对象的属性名与值紧挨排列。插入或删除任何一个 token都会让后续所有偏移失效——这也是System.Text.Json.JsonDocument只能整体重建的原因。REDox 的破局点在于 64 位双模式 token。看 DToken.cs 的结构定义一个 token 就是一个long高 3~4 位存放DTokenKind/DTokenTypeControl/Trivia/Boolean/Integer/String/Container……再往下是DTokenVariant16 位变体如IntegerHexadecimal、StringSingleQuote、ByteStringBase64低 56 位是载荷。README 的架构章节给出了这套设计的核心思想extension bit 0 → 载荷语义交给文档/格式层source-backed指向原始缓冲区 extension bit 1 → 载荷语义由 REDox 决定控制 token 支持格式无关的编辑insert/remove/replace也就是说同一个 token 形状既能表达指回源码缓冲区的一段切片的只读视图也能表达归 REDox 所有、可自由编辑的 DOM 节点。解析阶段token 只记 offset/length字符串和数字等数据一律延迟解码、按需物化一旦进入编辑token 便升级为扩展节点由引擎托管。关键的一步在容器视图。DArray、DMap、DObject都不是token 数组本身而是持有一个uint 数组存 token idDArray.cs 的_values是uint[]AddInternal/InsertInternal/RemoveAt做的不过是 slot 的追加、平移与收缩DObject.cs 与 DMap.cs 则是KeyValuePairuint,uint[]一个 key token id 一个 value token id属性查找时 DObject.cs 会惰性构建哈希桶EnsureIndex检测_indexedCount ! _count就重建索引插入删除后索引自动作废重算。这就是 README Mutable tape DOM 一节的再链接机制容器的扁平布局不变变的只是 slot 里指向的 token id。删除一个元素不需要压缩整个 token 流只要把这个 slot 里的 token 还给文档、然后把后面 slot 平移一格修改一个值等价于把 slot 重新指向一个新的 token。缓存友好的顺序访问特征保住了节点式操作也进来了。再看免重建的另一半——token slot 的复用。删除元素时Document.Internal.cs 的Free方法会把被释放的 token 改写为DToken.MakeEmpty(...)/DToken.MakeEmptyExtend(...)挂进一条单链表式自由列表_emptyTokenId/_emptyExtendId下一次AllocEmptyToken会优先从自由列表头部取出一个 slot 直接复用而不是追加扩展 token 数组。于是反复增删一个高频改动的配置节点GC 压力趋近于零token 数组也不会无限膨胀。配合 README 的 30 秒示例整个编辑面是这样的using var doc JsonDocument.Parse(json); var root doc.RootElement.AsObject(); root[Name] Claire; // 原地替换slot 重指向 root.Add(Hp, 100); // 追加属性 root.Remove(Level); // 删除属性 var items root[Items].AsArray(); items.Add(First Aid Spray); // 追加 items.Insert(0, Knife); // 头部插入 items.RemoveAt(1); // 按下标删除甚至允许跨类型替换DValue.ReplaceWithTDValue.cs会把句柄对应的 token 原地交给Document.JoinAny重写字符串换成对象、对象换成数组都只是一个 token 的改写。对游戏配置这类高频编辑场景这意味着每次改配置都要重新解析一遍 JSON成为过去式。性能上的收益是可量化的。以 README 公布的基准BenchmarkDotNet、.NET 10、Threadripper PRO 5975WX为准canada.json反序列化约 1.68x、自动并行约 2.84x序列化约 1.06x同样数据集上 REDox 反序列化分配约 2.6MB而System.Text.Json约 8.7MB。二、Version 版本安全机制一次编辑全局失效可变 DOM 最大的隐患不是能不能改而是改了之后别人手里的旧句柄怎么办。设想一个场景你拿到了doc.RootElement.AsValue()接着另一个代码路径删除了这个元素然后你继续用旧句柄去读它——读到的可能是被Free回收、又被AllocEmptyToken复用给完全不同节点的同一个 token slot。这比读到脏数据更糟这是把别人的数据当成你的。REDox 的解法是文档级单调递增的Version 版本号。在 Document.Internal.cs 里internal int Version { get; private set; }Version的每一次递增都意味着文档发生了结构性修改Free回收任意 token 时VersionDispose释放文档、Reset重置文档时Version换入整段 token 数组的ReplaceTokens也会失效父表缓存。而所有暴露给用户的句柄在创建的那一刻就拍下版本快照DElement.csinternal int Version { get; }IsValid的实现只有一行比较——Document.Version VersionDValue.csDValue(Document doc, uint id)构造时把版本直接编进Payload高 32 位Payload id | ((long)doc.Version 32)IsValid同样只需一次比较DTrivia.cs注释句柄DTrivia也持有_versionIsValid同样是_doc.Version _version。这套机制的本质是用一个 O(1) 的全局计数器做失效广播不需要逐个验证 token 是否仍归属于某个容器、不需要遍历检查 slot 是否被复用一次编辑让所有旧句柄在同一瞬间集体作废。更巧妙的是它把版本号嵌进了DValue的载荷连额外的字段开销都省了。失效之后的访问路径也写得很清楚DElement的IsValid为假时ToString、GetArrayLength、索引器统统走ThrowIfInvalid()抛出的不是暧昧的异常而是ObjectDisposedException。这比System.Text.Json的语义更进一步——JsonElement只有在文档Dispose之后才失效REDox 是每一次编辑都会让旧句柄立即失效从根上杜绝脏读与幽灵节点。对照测试 JsonElementCompatibility.cs 里的DisposeTest解析文档释放后DElement与JsonElement一样会抛ObjectDisposedException而文档活跃期间任何编辑都会经由Version让此前取出的句柄同步失效。版本号还顺带充当了懒缓存的脏标记。Document的父表_parentTable用于GetPath()等 O(1) 祖先查询它的重建条件是_parentVersion ! _tokenPttoken 水位一变就重建但不会在每次编辑时都立即重建把开销摊到真正需要读取祖先信息的时刻。这一设计说明版本号在 REDox 里不是孤立的句柄守卫而是一套贯穿句柄安全 缓存失效的统一时钟。而编辑即失效的另一面是快照能力。可变 DOM 配上 Document.cs 的CreateSnapshotT按 token 数组做一次浅拷贝、深复制扩展表就能拿到一个与当前文档完全独立、可继续编辑的副本。社区对 REDox DOX 二进制格式的讨论中反复出现的撤销/存档/快照场景正是建立在这对组合之上——要支持撤销先得有低成本生成上一版本的能力。三、JSON5 注释保留保真度不是免费的Token DOM 的可编辑性一旦成立下一个自然的追问是编辑之后那些不是数据的数据怎么办游戏项目里的配置文件几乎必然带注释和讲究的排版用普通 JSON 解析器改一次注释就人间蒸发。REDox 用 JSON5 的 Trivia 机制回答了这个问题——但它给出的答案里写满了取舍。Token 模型里Trivia 是一等公民DTokenKind.Trivia之下细分了Whitespace、BlockComment、LineComment、Separator等变体见 DTokenKind.cs每个 token 的扩展载荷里甚至可以带一个TriviaId指向注释列表。解析侧Json5DocumentOptions.cs 提供了PreserveTrivia开关写出侧Json5WriteOptions.cs 也有对应的PreserveTrivia。默认值是false。为什么因为在 Document.Internal.cs 的CreateLeadingTrivia里能看到代价为保留注释引擎需要在扩展表_extends中为每个携带 Trivia 的 token 分配一个Listuint把注释 token 一个个串进去。这既吃掉一块托管堆分配也让 token 的TriviaId字段和扩展表条目占据位宽与内存。对解析即走的只读场景这是纯浪费——所以默认关掉换取紧凑的 token 表和零额外分配只有明确要改完还能把注释原样写回来时才显式打开。打开之后的行为在测试 Json5WriteTest.cs 里有完整呈现。ParseTrivia用例解析了一段带//Json5头注释和行内注释的 JSON5遍历number属性的LeadingTrivia能看到每条注释的Kind与原文随后执行number.AsValue().ReplaceWith(567)—— 值从字符串换成了数字而同一段leadingTrivia枚举依然能读出全部注释。注释与值解耦存储编辑只改写值的 tokenTrivia 列表原样保留。DTriviaCollectionDTriviaCollection.cs甚至开放了AddFirst/AddLast/Clear让程序可以在编辑的同时补写新的注释。Trivia 只是保真的一部分。回看 DTokenVariant.cs会发现 REDox 把格式细节也编码进了 token 变体IntegerHexadecimal/IntegerOctal/IntegerBinary让数字保留原始进制StringSingleQuote/StringMultilineDoubleQuote让字符串记住引号风格FloatHalf/FloatSingle保留浮点宽度。配合Json5WriteOptions的PropertyNameStyle/StringStyleAutoOrSingle/AlwaysDouble等输出层可以决定是否沿用输入时的风格。这套变体即元数据的设计本质上是把词法保真成本显式地放进 token 位宽默认路径不付出任何代价需要保真时再按需启用。取舍的天平两端都摆得很清楚保真度是能力不是默认值。对绝大多数只读/序列化场景丢弃 Trivia 是正确默认对需要改了还能保留注释的配置编辑场景一次性打开开关、承担扩展表分配换来回写后仍人畜无害的配置文件。四、设计哲学小结把三块拼图放在一起REDox 可变 Token DOM 的设计哲学可以提炼为三个词双模式同一个 64 位 token既是源码切片视图也是编辑所有权视图。只读路径零物化编辑路径按需升级互不拖累。时钟即安全文档级 Version 作为全局失效时钟句柄创建时拍照、编辑时递增、读取时比对。用一次整数比较换取脏读不可能发生的强保证同时复用同一时钟驱动懒缓存失效。保真按需付费Trivia、引号风格、数字进制这类非数据信息统统建模为可选变体默认关闭、显式开启把代价的选择权交给调用方。对 .NET 生态而言这套可变 tape DOM 版本句柄的架构提供了一个少见的设计样本它证明了解析性能、编辑能力与格式保真不必三选一——前提是愿意像 REDox 这样把数据、格式细节与失效语义都压进那 64 位 token 和一枚版本号里。【免费下载链接】REDoxHigh-performance, token-based structured data engine for .NET. A core component of REX, the technology behind CAPCOMs next-generation game engine.项目地址: https://gitcode.com/gh_mirrors/redox/REDox创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →