github.com/cloudwego/dynamicgo@v0.2.6-0.20240519101509-707f41b6b834/proto/INTRODUCTION.md (about) 1 # DynamicGo For Protobuf Protocol 2 该模块实现了dynamicgo对于高性能protobuf协议泛化调用的支持,由于protobuf协议与thrift协议的编码格式和规范存在较大差异,这里我们将详细介绍dynamicgo对于protobuf协议的相关设计细节和[飞书设计文档](https://zt69na939u.feishu.cn/docx/EzzZdEFDDofpFtxjFVFcMjQgnNd) 3 4 ## protobuf协议重要细节 5 在介绍链路设计之前我们有必要先了解一下protobuf协议的规范,后续的各项设计都将在严格遵守该协议的规范的基础上进行改进。 6 7 ## Protobuf源码链路 8 Protobuf源码链路过程主要涉及三类数据类型之间的转换,以Message对象进行管理,自定义Value类实现反射和底层存储,用Message对象能有效地进行管理和迁移,但也带来了许多反射和序列化开销。 9 10  11 12 ### Protobuf编码格式 13 protobuf编码字段格式大体上遵循TLV(Tag,Length,Value)的结构,但针对具体的类型存在一些编码差异,这里我们将较为全面的给出常见类型的编码模式。 14 15 #### Message Field 16 protobuf的接口必定包裹在一个Message类型下,因此无论是request还是response最终都会包裹成一个Message对象,那么Field则是Message的一个基本单元,任何类型的字段都遵循这样的TLV结构: 17  18 - Tag由字段编号Field_number和wiretype两部分决定,对位运算后的结果进行varint压缩,得到压缩后的字节数组作为字段的Tag。 19 - wiretype的值则表示的是value部分的编码方式,可以帮助我们清楚如何对value的读取和写入。**到目前为止,wiretype只有VarintType,Fixed32Type,Fixed64Type,BytesType四类编码方式。** 20 - 其中属于VarintType类型的value会被压缩后再编码。 21 - 属于Fixed32Type和Fixed64Typ固定长度类型的value分布占用4字节和8字节。 22 - 属于不定长编码类型BytesType的则会编码计算value部分的ByteLen再拼接value。 23 - ByteLen用于编码表示value部分所占的字节长度,同样我们的bytelen的值也是经过varint压缩后得到的,但bytelen并不是每个字段都会带有,只有不定长编码类型BytesType才会编码bytelen。**ByteLen由于其不定长特性,计算过程在序列化过程中是需要先预先分配空间,记录位置,等写完内部字段以后再回过头来填充。** 24 - Value部分是根据wiretype进行对应的编码,字段的value可能存在嵌套的T(L)V结构,如字段是Message,Map等情况。 25 #### Message 26 关于Message本身的编码是与上面提到的字段编码一致的,也就是说遇到一个Message字段时,我们会先编码Message字段本身的Tag和bytelen,然后再来逐个编码Message里面的每个字段的TLV。**但需要提醒注意的是,在最外层不存在包裹整个Message的Tag和bytelen前缀,只有每个字段的TLV拼接。** 27 28 #### List 29 list字段比较特殊,protobuf为了节省存储空间,根据list元素的类型分别采用不同的编码模式。 30 - Packed List Mode 31 如果list的元素value本身属于VarintType/Fixed32Type/Fixed64Type编码格式,那么将采用packed模式编码整个List,在这种模式下的list是有bytelen的。protobuf3默认对这些类型的list启用packed。 32  33 - UnPacked List Mode 34 **当list元素属于BytesType编码格式时**,list将使用unpacked模式,直接编码每一个元素的TLV,这里的V可能是嵌套的如List<Message>模式,**那么unpacked模式下所有元素的tag都是相同的,list字段的结束标志为与下一个TLV字段编码不同或者到达buf末尾。** 35  36 37 #### Map 38 Map编码模式与unpacked list相同,根据官方文档所说,Map的每个KV键值对其实本质上就是一个Message,固定key的Field_number是1,value的Field_number是2,那么Map的编码模式就和`List<Message>`一致了。 39  40 41 42 43 ### Descriptor细节 44 这里主要介绍一下源码的descriptor设计上的一些需要注意的细节,有助于理解我们自定义descriptor的方案。 45 - Service接口由ServiceDescriptor来描述,ServiceDescriptor当中可以拿到每个rpc函数的MethodDescriptor。 46 - MethodDescriptor中Input()和output()两个函数返回值均为MessageDescriptor分别表示request和response。 47 - MessageDescriptor专门用来描述一个Message对象(也可能是一个MapEntry),可以通过Fields()找到每个字段的FieldDescriptor。 48 - FieldDescriptor兼容所有类型的描述。 49 50  51 52 **FieldDescriptor注意要点:** 53 1. 如果字段是一个List,那么FieldDescriptor中Kind()表示的是元素的Kind(),字段是否是List只能通过IsList()函数区分,即当字段为List<Message>的情况,那么此时调用Kind()得到的为messagekind。 54 2. 如果字段是Map,那么FieldDescriptor中Kind()表示为messageKind,字段是否是List只能通过IsMap()函数区分。 55 3. 当字段为messagekind时(可能的情况message,List<Message>,map<int/string,Message>),可以通过Message()方法,可以获得下层嵌套的MessageDescriptor。 56 4. MapKey的FieldDescriptor被限定为uint/bool/int/string类型。目前的代码设计上将uint32,uint64,int32,int64全都转成了int,bool没处理。 57 58 ## Descriptor自定义设计 59 Descriptor的设计原理基本与源码一致,不过为了兼容性和避免强转,这里我们通thrift抽象了一个TypeDescriptor来处理类型问题,但因为protobuf编码格式的问题,TypeDescriptor做了一部分修改。 60 61 parse过程主要利用的是[`github.com/jhump/protoreflect@v1.8.2`](https://pkg.go.dev/github.com/jhump/protoreflect@v1.8.2)解析得到的第三方descriptor进行的再封装,从实现过程上来看,与高版本protoreflect利用[`protocompile`](https://pkg.go.dev/github.com/bufbuild/protocompile)对原始链路再make出源码的warp版本一致,更好的实现或许是处理利用protoreflect中的ast语法树构造。 62 63 ### TypeDescriptor 64 ```go 65 type TypeDescriptor struct { 66 baseId FieldNumber // for LIST/MAP to write field tag by baseId 67 typ Type 68 name string 69 key *TypeDescriptor 70 elem *TypeDescriptor 71 msg *MessageDescriptor // for message, list+message element and map key-value entry 72 } 73 ``` 74 75 - `baseId`:因为对于LIST/MAP类型的编码特殊性,如在unpack模式下,每一个元素都需要编写Tag,我们必须在构造时针对LIST/MAP提供fieldnumber,来保证读取和写入的自反射性。 76 - `msg`:这里的msg不是仅Message类型独有,主要是方便J2P部分对于List<Message>和marshalto中map获取可能存在value内部字段缺失的MapEntry的MassageDescriptor(在源码的设计理念当中MAP的元素被认为是一个含有key和value两个字段的message)的时候能利用TypeDescriptor进入下一层嵌套。具体位置:[j2p for List<Message>](../conv/j2p/decode.go#L422)和[map entry marshalto](./generic/value.go#L853)。 77 - `typ`:这里的Type不同于源码的FieldDescriptor,对LIST/MAP做了单独定义,对应`type.go`文件下的[`Type`](./type.go#L87)。 78 79 ### FieldDescriptor 80 ```go 81 type FieldDescriptor struct { 82 kind ProtoKind // the same value with protobuf descriptor 83 id FieldNumber 84 name string 85 jsonName string 86 typ *TypeDescriptor 87 } 88 ``` 89 90 - FieldDescriptor的设置是希望变量和函数作用与源码FieldDescriptor基本一致,只是增加`*TypeDescriptor`可以更细粒度的反应类型以及对FieldDescriptor的API实现。 91 - `kind`:保留kind,对应`type.go`文件下的[`ProtoKind`](./type.go#L41),LIST情况下,kind是列表元素的kind类型,MAP和MESSAGE情况下,messagekind。 92 93 ## ProtoBuf DOM设计 94 显然protobuf的层层包裹设计与thrift差异很大,这也给DOM设计造成了许多的困难,虽然思想上和dynamicgo-thrift一致,但为了更统一化的处理节点和片段管理,这里我们对链路的存储格式和函数设计做了一些改进。 95 ### 存储结构 96 #### Node结构 97 ```go 98 type Node struct { 99 t proto.Type 100 et proto.Type 101 kt proto.Type 102 v unsafe.Pointer 103 l int // ptr len 104 size int // only for MAP/LIST element counts 105 } 106 ``` 107 108 **具体的存储规则如下:** 109 - 基本类型Node表示:指针v的起始位置不包含tag,指向(L)V,t = 具体类型,et,kt = UNKNOWN 110 - Message类型Node:指针v的起始位置不包含tag,指向(L)V,如果是root节点,那么v的起始位置本身没有前缀的L,直接指向了V即第一个字段的tag上,而其余子结构体都包含前缀L。 111 - t = MESSAGE,et,kt = UNKNOWN 112 - List类型Node:为了兼容List的两种模式和自反射的完整性,我们必须包含list的完整片段和tag。**因此List类型的节点,v指向list的tag上,即如果是packed模式就是list的tag上,如果是unpacked则在第一个元素的tag上。** 113 - t = LIST,et = 元素类型,kt = UNKNOWN,size = 元素个数。 114 - Map类型Node:同理,Map的指针v也指向了第一个pair的tag位置。 115 - t = MAP ,kt = Key类型 et = Value类型,size = 元素个数。 116 - UNKNOWN类型Node:无法解析的合理字段Node会将TLV完整存储,多个相同字段Id的会存到同一节点,缺点是内部的子节点无法构建,同官方源码unknownFields原理一致。 117 - ERROR类型Node:基本与thrift一致,在setnotfound中,若片段设计与上述规则一致则可正确构造插入。 118 - 虽然MAP/LIST的父级Node存储有些变化,但是其子元素节点都是基本类型/MESSAGE,所以**DOM当中的所有叶子节点存储格式还是基本的(L)V。** 119 120 #### Value结构 121 value的结构本身是对Node的封装,将Node与相应的descriptor封装起来,但不同于thrift,在protobuf当中由于片段无法完全自解析出具体类型,之后的涉及到具体编码的部分操作不能脱离descriptor,只能实现Value类作为调用单位。 122 123 ```go 124 type Value struct { 125 Node 126 Desc *proto.TypeDescriptor 127 IsRoot bool 128 } 129 130 func NewRootValue(desc *proto.TypeDescriptor, src []byte) Value { 131 return Value{ 132 Node: NewNode(proto.MESSAGE, src), 133 Desc: desc, 134 IsRoot: true, 135 } 136 } 137 138 // only for basic Node 139 func NewValue(desc *proto.TypeDescriptor, src []byte) Value { 140 return Value{ 141 Node: NewNode(desc.Type(), src), 142 Desc: desc, 143 } 144 } 145 ``` 146 由于从rpc接口解析后我们直接得到了对应的TypeDescriptor,再加上root节点本身没有前缀TL的独特编码结构,我们设计`IsRoot`来区分root节点和其余节点,也能避免利用父类强转。 147 148 #### Path与PathNode 149 结构功能均与thrift一致,且protobuf不存在PathBinKey。 150 ```go 151 // Path represents the relative position of a sub node in a complex parent node 152 type Path struct { 153 t PathType // 类似div标签的类型,用来区分field,map,list元素,帮助判定父级嵌套属于什么类型结构 154 155 v unsafe.Pointer // PathStrKey, PathFieldName类型,存储的是Key/FieldName的字符串指针 156 157 l int 158 // PathIndex类型,表示LIST的下标 159 // PathIntKey类型,表示MAPKey的数值 160 // PathFieldId类型,表示字段的id 161 } 162 163 pathes []Path : 合理正确的Path数组,可以定位到嵌套复杂类型里具体的key/index的位置 164 ``` 165 ### 构建DOM Tree 166 #### PathNode.Load/Value.Children 167 构建DOM支持懒加载和全加载即recurse参数。 168 - 在懒加载模式下LIST/MAP的Node当中size不会同步计算,因为字段会直接skip,而全加载在构造叶子节点的同时顺便更新了size,但不影响后续的迭代器反射,后续迭代器翻译还是以Node片段长度为准,而不是直接看size。 169 - LIST/MAP的节点包含tag,代码中hindlechild函数会传入一个tagL参数,遇到LIST/MAP时取地址片段时会让start前移tag长度。 170 查找字段 171 172 ### 查找字段 173 #### GetByPath/GetByPathWithAddress/GetMany 174 支持任意Node查找,查找函数设计了三个外部API:GetByPath,GetByPathWithAddress,GetMany。 175 - GetByPath函数:返回查找出来的Value,查找失败返回ERRORNode 176 - GetByPathWithAddress:返回Value和当前调用节点到查找节点过程中每个Path嵌套层的tag位置的偏移量。[]address与[]Path个数对应,若调用节点为root节点,那么都是到buf首地址的偏移量。、 177 - GetMany:传入当前嵌套层下合理的[]PathNode的Path部分,直接返回构造出来多个Node,得到完整的[]PathNode,可继续用于MarshalMany 178 179 #### 设计思路 180 查找过程是根据传入的[]Path来循环遍历查找每一层Path嵌套里面对应的位置,根据嵌套的Path类型(fieldId,mapKey,listIndex),调用不同的search函数。 181 - searchFieldId:找到字段,返回字段tag位置,未找到则返回message片段末尾。 182 - searchInt/strKey:找到字段,返回字段tag位置,未找到则返回map片段末尾。 183 - searchIndex:找到字段,若为pack则返回value起始位置,若为unpack返回元素tag位置,未找到则返回list片段末尾。 184 构造最终返回的Node时,根据具体类型看是否需要去除tag即可,返回的[]address刚好在search过程中完成了append每一层Path的tag偏移量。 185 186 ### 插入字段 187 #### SetByPath/setMany 188 两个外部API:SetByPath和SetMany。插入字段思路大体与thrift一致,**区别在于:为了保证兼容pack/unpack/map/message的统一性,不同于thrift,对于新增数据只能采用尾插法,完成插入操作后需要进行回写嵌套层bytelen的操作。** 189 - SetByPath:只支持root节点调用,保证UpdateByLen更新的完整和正确性。 190 - setMany:可支持局部节点插入,除了构造当前嵌套层下合理的PathNode,还需要额外传入rootValue的指针,目标插入节点到root节点的位置,以及到root的[]Path,完成多个节点插入后。判断当前调用节点是否为root,若不是则完成当前层的updatebytelen后,需要利用rootValue再调用一次updatebytelen。 191 192 #### Updatebytelen细节 193 - 计算插入完成后新的长度与原长度的差值,存下当前片段增加或者减少的diffLen。 194 - 从里向外逐步更新[]Path数组中存在bytelen的嵌套层(只有packed list和message)的bytelen字节数组。 195 - 更新规则:先readTag,然后再readLength,得到解varint压缩后的具体bytelen数值,计算newlen = bytelen + diffLen,计算newbytelen压缩后的字节长度与原bytelen长度的差值sublen,并累计diffLen+=sublen。 196 - 指针向前移动到下一个path和address。 197 198  199 200 ### 删除字段 201 #### UnsetByPath 202 目前只支持root节点调用。删除字段同插入字段,即找到对应的片段后直接将片段置空,然后更新updatebytelen。 203 204 ### PathNode序列化 205 对外API:Marshal和MarshalTo。 206 #### Marshal 207 DOM建好PathNode后,可直接调用PathNode的Marshal方法即可,本质是BFS遍历拼接DOM的所有叶子节点,Tag部分会通过Path类型和Node类型进行补全,bytelen会预先分配,根据实际遍历节点进行回写,特殊处理root层,不需要写message的TL前缀。 208 209  210 211 #### MarshalTo 212 MarshalTo主要用于裁剪字段,获取新旧descriptor中共有的字段id,然后进行翻译,可支持多层嵌套内部的字段缺失,LIST/MAP内部元素字段缺失,核心关键在于`message`,`List<Message>`,`map<int/string,Message>`的descriptor均为messageKind。 213 214 ## JSON协议转换 215 ### ProtoBuf——>JSON 216 #### 协议转换过程 217 协议转换的过程可以理解为逐字节解析 ProtoBuf,并结合 Descriptor 类型编码为 JSON 到输出字节流,整个过程是 in-place 进行的,并且结合内存池技术,仅需为输出字节流分配一次内存即可。 218 ProtoBuf——>JSON 的转换过程如下: 219 1. 根据输入的 Descriptor 指针类型区分,若为 Singular(string/number/bool/Enum) 类型,跳转到 220 2. 按照 Message([Tag] [Length] [TLV][TLV][TLV]....)编码格式对输入字节流执行 varint解码,将Tag解析为 fieldId(字段ID)、wireType(字段wiretype类型); 221 3. 根据第2步解析的 fieldId 确定字段 FieldDescriptor,并编码字段名 key 作为 jsonKey 到输出字节流; 222 4. 根据 FieldDescriptor 确定字段类型(Singular/Message/List/Map),选择不同编码方法编码 jsonValue 到输出字节流; 223 5. 如果是 Singular 类型,直接编码到输出字节流; 224 6. 其它类型递归处理内部元素,确定子元素 Singular 类型进行编码,写入输出字节流中; 225 7. 及时输入字节流读取位置和输出字节流写入位置,跳回2循环处理,直到读完输入字节流。 226 227 #### List类型处理 228 > List类型包括 PackedList、UnpackedList,区别为元数据类型是否为 Number。在转换时根据 FieldDescriptor 区分这两种类型; 229 - PackedList:[Tag][Length][Value Value Value Value ....] 230 解析完 Tag、Length后循环处理 Value数组,每解析一个 Value元素,编码到输出字节流; 231 - UnpackedList:[Tag][Length][Value] [Tag][Length][Value] ... 232 所有子节点共享 fieldId(字段ID),因此每次预解析 Tag 得到 elementNumber,并以 (elementNumber==fieldId)=true 作为循环条件直到结束。 233 234 #### Map类型处理 235 Map:[PairTag][PairLength][KeyTag][KeyLength][KeyValue][ValueTag][ValueLength][ValueValue].... 236 237 Map 类型所有 PairTag 共享相同 fieldId(字段ID)。因此预解析 PairTag 得到 pairNumber,并以 (pairNumber==fieldId)=true 作为循环条件直到结束。 238 239 ### JSON——>ProtoBuf 240 协议转换过程中通过 [sonic](https://github.com/bytedance/sonic) 框架实现 in-place 遍历 JSON,并实现了接口 Onxxx(OnBool、OnString、OnInt64....)方法达到编码 ProtoBuf 的目标,编码过程也做到了 in-place。 241 242 #### VisitorUserNode 结构 243 因为在编码 Protobuf 格式的 Mesage/UnpackedList/Map 类型时需要对字段总长度回写,并且在解析复杂类型(Message/Map/List)的子元素时需要依赖复杂类型 Descriptor 来获取子元素 Descriptor,所以需要 VisitorUserNode 结构来保存解析 json 时的中间数据。 244 245 ```go 246 type VisitorUserNode struct { 247 stk []VisitorUserNodeStack 248 sp uint8 249 p *binary.BinaryProtocol 250 globalFieldDesc *proto.FieldDescriptor 251 } 252 ``` 253 1. stk:记录解析时中间变量的栈结构,在解析 Message 类型时记录 MessageDescriptor、PrefixLen;在解析 Map 类型时记录 FieldDescriptor、PairPrefixLen;在解析 List 类型时记录 FieldDescriptor、PrefixListLen; 254 2. sp:当前所处栈的层级; 255 3. p:输出字节流; 256 4. globalFieldDesc:每当解析完 MessageField 的 jsonKey 值,保存该字段 Descriptor 值; 257 258 #### VisitorUserNodeStack 结构 259 记录解析时字段 Descriptor、回写长度的起始地址 PrefixLenPos 的栈结构 260 ```go 261 type VisitorUserNodeStack struct { 262 typ uint8 263 state visitorUserNodeState 264 } 265 ``` 266 1. typ:当前字段的类型,取值有对象类型(objStkType)、数组类型(arrStkType)、哈希类型(mapStkType); 267 2. state:存储详细的数据值; 268 269 #### visitorUserNodeState 结构 270 ```go 271 type visitorUserNodeState struct { 272 msgDesc *proto.MessageDescriptor 273 fieldDesc *proto.FieldDescriptor 274 lenPos int 275 } 276 ``` 277 1. msgDesc:记录 root 层的动态类型描述 MessageDescriptor; 278 2. fieldDesc:记录父级元素(Message/Map/List)的动态类型描述 FieldDescriptor; 279 3. lenPos:记录需要回写 PrefixLen 的位置; 280 281 #### 协议转换过程 282 JSON——>ProtoBuf 的转换过程如下: 283 1. 从输入字节流中读取一个 json 值,并判断其具体类型(object/array/string/float/int/bool/null); 284 2. 如果是 object 类型,可能对应 ProtoBuf MapType/MessageType,sonic 会按照 `OnObjectBegin()——>OnObjectKey()——>decodeValue()...` 顺序处理输入字节流。 285 - OnObjectBegin()阶段解析具体的动态类型描述 FieldDescriptor 并压栈; 286 - OnObjectKey() 阶段解析 jsonKey 并以 ProtoBuf 格式编码 Tag、Length 到输出字节流; 287 - decodeValue()阶段递归解析子元素并以 ProtoBuf 格式编码 Value 部分到输出字节流,若子类型为复杂类型(Message/Map),会递归执行第 2 步;若子类型为复杂类型(List),会递归执行第 3 步。 288 3. 如果是 array 类型,对应 ProtoBuf PackedList/UnpackedList,sonic 会按照 `OnObjectBegin()——>OnObjectKey()——>OnArrayBegin()——>decodeValue()——>OnArrayEnd()...` 顺序处理输入字节流。 289 - `OnObjectBegin()`阶段处理解析 List 字段对应动态类型描述 FieldDescriptor 并压栈; 290 - `OnObjectKey()`阶段解析 List 下子元素的动态类型描述 FieldDescriptor 并压栈; 291 - `OnArrayBegin()`阶段将 PackedList 类型的 Tag、Length 编码到输出字节流; 292 - `decodeValue()`阶段循环处理子元素,按照子元素类型编码到输出流,若子元素为复杂类型(Message),会跳转到第 2 步递归执行。 293 4. 在结束处理某字段数据后执行 `onValueEnd()、OnArrayEnd()、OnObjectEnd()`,获取栈顶 lenPos 数据,对字段长度部分回写并退栈。 294 5. 更新输入和输出字节流位置,跳回第 1 步循环处理,直到处理完输入流数据。 295 296 ## 性能测试 297 构造与thrift性能测试基本相同的[baseline.proto](../testdata/idl/baseline.proto)文件,定义了对应的简单([Small](../testdata/idl/baseline.proto#L7))、复杂([Medium](../testdata/idl/baseline.proto#L22))、简单缺失([SmallPartial](../testdata/idl/baseline.proto#L16))、复杂缺失([MediumPartial](../testdata/idl/baseline.proto#L40)) 两个对应子集,并用kitex命令生成了对应的[baseline.pb.go](../testdata/kitex_gen/pb/baseline/baseline.pb.go)。 298 主要与Protobuf-Go官方源码进行比较,部分测试与kitex-fast也进行了比较,测试环境如下: 299 - OS:Windows 11 Pro Version 23H2 300 - GOARCH: amd64 301 - CPU: 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz 302 - Go VERSION:1.20.5 303 304 ### 反射 305 - 代码:[dynamicgo/testdata/baseline_pg_test.go](../testdata/baseline_pg_test.go) 306 - 图中列举了DOM常用操作的性能,测试细节与thrift相同。 307 - MarshalTo方法:相比protobufGo提升随着数据规模的增大趋势越明显,ns/op开销约为源码方法的0.29 ~ 0.32。 308 309  310 311 ### 字段Get/Set定量测试 312 - 代码:[dynamicgo/testdata/baseline_pg_test.go#BenchmarkRationGet_DynamicGo](../testdata/baseline_pg_test.go#L1607) 313 - [factor](../testdata/baseline_pg_test.go#L1563)用于修改从上到下扫描proto文件字段获取比率。 314 - 定量测试比较方法是protobufGo的dynamicpb模块和DynamicGo的Get/SetByPath,SetMany,测试对象是medium data的情况。 315 - Set/Get字段定量测试结果均优于ProtobufGo,且在获取字段越稀疏的情况下性能加速越明显,因为protobuf源码不论获取多少比率的字段,都需要完整序列化全体对象,而dynamicgo则是直接解析buf完成copy。 316 - setmany性能加速更明显,在100%字段下ns/op开销约为0.11。 317 318  319 320 ### 序列化/反序列化 321 - 代码:[dynamicgo/testdata/baseline_pg_test.go#BenchmarkProtoMarshalAll_DynamicGo](../testdata/baseline_pg_test.go#L1212) 322 - 序列化在small规模略高于protobufGo,medium规模的数据上性能优势更明显,ns/op开销约为源码的0.54 ~ 0.84。 323 - 反序列化在reuse模式下,small规模略高于protobufGo,在medium规模数据上性能优势更明显,ns/op开销约为源码的0.44 ~ 0.47,随数据规模增大性能优势增加。 324  325  326 327 ### 协议转换 328 - Json2Protobuf优于ProtobufGo,ns/op性能开销约为源码的0.21 ~ 0.89,随着数据量规模增大优势增加。 329 - 代码:[dynamicgo/testdata/baseline_j2p_test.go](../testdata/baseline_j2p_test.go) 330 - Protobuf2Json性能明显优于ProtobufGo,ns/op开销约为源码的0.13 ~ 0.21,而相比Kitex,ns/op约为Sonic+Kitex的0.40 ~ 0.92,随着数据量规模增大优势增加。 331 - 代码:[dynamicgo/testdata/baseline_p2j_test.go](../testdata/baseline_p2j_test.go) 332 333  334 335 详细数据结果文件可在飞书文档中下载。 336 337 ## TODO 338 - [ ] 添加JSON协议的HttpMapping。 339 - [ ] 修改为TypeDescriptor后,J2P由于sonic限定接口`Onxxx`,为了write tag,暂时还是只能用fielddescriptor,兼容其他类型的建议不放到snoic当中,而是走其他分支直接用binaryprotocol写入比较好。 340 - [ ] 部分JSON option暂未实现。 341 - [ ] DOM tree的Assign函数还需要实现,尝试实现但存在一些问题由于时间关系不太好解决。 342 - [ ] 序列化/反序列化在small数据规模下的开销进一步优化。 343 - [ ] 是否可以优化到不区分DOM的Node不区分root层和其他层的Message?目前的root层MESSAGE的Node不带L,而其他的都带有L,感觉可以优化之前设计没做好可能,如果统一可能有别的地方会带来些小问题,比如在非递归的DOM下marshal则需要自己补充L,因为Path只补齐了Tag。 344 - [ ] 用Kind的地方设计如何方便地完全被TypeDescriptor替代。 345 - [ ] DOM对于Enum,Oneof,Extension(Extension已经在protobuf3官方弃用)的支持。 346 - [ ] DOM相关的部分option有待实现。