首页 » 智能 » Go 标准库 encoding/json 真的慢吗?_序列化_机能

Go 标准库 encoding/json 真的慢吗?_序列化_机能

落叶飘零 2024-11-17 10:47:22 0

扫一扫用手机浏览

文章目录 [+]

插图来自于“A Journey With Go”,由 Go Gopher 组织成员 Renee French 创作。

本文基于 Go 1.12。

Go 标准库 encoding/json 真的慢吗?_序列化_机能 智能

关于标准库 encoding/json 性能差的问题在很多地方被谈论过,也有很多第三方库在考试测验办理这个问题,比如easyjson[1],jsoniter[2]和ffjson[3]。
但是标准库 encoding/json 真的慢吗?它一贯都这么慢吗?

标准库 encoding/json 的进化之路

首先,通过一个简短的 makefile 文件和一段基准测试代码,我们看下在各个 Go 版本中,标准库 encoding/json 的性能表现。
以下为基准测试代码:

typeJSONstruct{FoointBarstringBazfloat64}funcBenchmarkJsonMarshall(btesting.B){j:=JSON{Foo:123,Bar:`benchmark`,Baz:123.456,}b.ResetTimer()fori:=0;i<b.N;i++{_,_=json.Marshal(&j)}}funcBenchmarkJsonUnmarshal(btesting.B){bytes:=`{"foo":1,"bar":"mystring",bar:1.123}`str:=[]byte(bytes)b.ResetTimer()fori:=0;i<b.N;i++{j:=JSON{}_=json.Unmarshal(str,&j)}}

makefile 文件在不同的文件夹中基于不同版本的 Go 创建 Docker 镜像,在各镜像启动的容器中运行基准测试。
将从以下两个维度进行性能比拟:

比较 Go 各版本与 1.12 版本中标准库 encoding/json 的性能差异比较 Go 各版本与其下一个版本中标准库 encoding/json 的性能差异

第一个维度的比拟可以得到在特定版本的 Go 与 1.12 版本的 Go 中 json 序列化和反序列化的性能差异;第二个维度的比拟可以得到在哪次 Go 版本升级中 json 序列化和反序列化发生了最大的性能提升。

测试结果如下:

Go1.2 至 Go1.3 的版本升级,序列化操作耗时减少了约 28%,反序列化操作耗时减少了约 35%

nameoldtime/opnewtime/opdeltaJsonMarshall1.91µs±2%1.37µs±2%-28.23%JsonUnmarshal2.70µs±2%1.75µs±3%-35.18%Go1.6 至 Go1.7 的版本升级,序列化操作耗时减少了约 27%,反序列化操作耗时减少了约 40%

nameoldtime/opnewtime/opdeltaJsonMarshall-41.24µs±1%0.90µs±2%-27.65%JsonUnmarshal-41.52µs±3%0.91µs±2%-40.05%Go1.10 至 Go1.11 的版本升级,序列化内存花费减少了约 60%,反序列化内存花费减少了约 25%

nameoldalloc/opnewalloc/opdeltaJsonMarshall-4208B±0%80B±0%-61.54%JsonUnmarshal-4496B±0%368B±0%-25.81%Go1.11 至 Go1.12 的版本升级,序列化操作耗时减少了约 15%,反序列化操作耗时减少了约 6%

nameoldtime/opnewtime/opdeltaJsonMarshall-4670ns±6%569ns±2%-15.09%JsonUnmarshal-4800ns±1%747ns±1%-6.58%

可以在这里看到完全的测试结果[4]。

如果比拟 Go1.2 与 Go1.12,会创造标准库 encoding/json 的性能有显著提高,操作耗时减少了约 69%/68%,内存花费减少了约 74%/29%:

nameoldtime/opnewtime/opdeltaJsonMarshall1.72µs±2%0.52µs±2%-69.68%JsonUnmarshal2.72µs±2%0.85µs±5%-68.70%nameoldalloc/opnewalloc/opdeltaJsonMarshall188B±0%48B±0%-74.47%JsonUnmarshal519B±0%368B±0%-29.09%

该基准测试利用了较为大略的 json 构造。
利用更加繁芜的构造(例如:Map or Array)进行测试会导致各版本之间性能增幅与本文不同。

速读源码

想理解标准库性能较差的缘故原由的最好的办法便是读源码,以下为 Go1.12 版本中 json.Marshal函数的实行流程:

在理解了 json.Marshal 函数的实行流程后,再来比较下在 Go1.10 和 Go1.12 版本中的 json.Marshal 函数在实现上有什么变革。
通过之前的测试,可以创造从 Go1.10 至 Go1.12 版本中的 json.Marshal 函数的内存花费上有了很大的改进。
从源码的变革中可以创造在 Go1.12 版本中的 json.Marshal 函数添加了 encoder(编码器)的内存缓存:

在利用了 sync.Pool 缓存 encoder 后,json.Marshal 函数极大地减少了内存分配操作。
实际上 newEncodeState() 函数在 Go1.10 版本中就已经存在了[5],只不过没有被利用。
为验证是添加了内存缓存带来了性能提升的猜想,可以在 Go1.10 版本中修正 json.Marshal 函数后,再进行测试:

nameoldalloc/opnewalloc/opdeltaCodeMarshal-44.59MB±0%1.98MB±0%-56.92%

可以直接在Go 源码[6]中,实行以下命令进行基准测试:

gotestencoding/json-bench=BenchmarkCodeMarshal-benchmem-count=10-run=^$

结果和我们的猜想是同等的。
是sync 包[7]给 json.Marshal 函数带来了性能提升。
同样也给我们带来一点启示,当项目也有这种对同一个构造体进行大量的内存分配时,也可以通过添加内存缓存的办法提升性能。

以下为 Go1.12 版本中,json.Unmarshal 函数的实行流程:

json.Unmarshal 函数同样利用 sync.Pool 缓存了 decoder。
对付 json 序列化和反序列化而言,其性能瓶颈是迭代、反射 json 构造中每个字段。

与第三方库的性能比拟

GitHub 上也有很多用于 json 序列化的第三方库,比如ffjson[8]便是个中之一,ffjson 的命令行工具可以为指定的构造天生静态的 MarshalJSON 和 UnmarshalJSON 函数,MarshalJSON 和 UnmarshalJSON 函数在序列化和反序列化操作时会分别被 ffjson.Marshal 和ffjson.Unmarshal 函数调用。
以下为 ffjson 天生的解析器示例:

func(jJSONFF)MarshalJSON()([]byte,error){varbuffflib.Bufferifj==nil{buf.WriteString("null")returnbuf.Bytes(),nil}err:=j.MarshalJSONBuf(&buf)iferr!=nil{returnnil,err}returnbuf.Bytes(),nil}//MarshalJSONBufmarshalbufftoJSON-templatefunc(jJSONFF)MarshalJSONBuf(buffflib.EncodingBuffer)error{ifj==nil{buf.WriteString("null")returnnil}varerrerrorvarobj[]byte_=obj_=errbuf.WriteString(`{"Foo":`)fflib.FormatBits2(buf,uint64(j.Foo),10,j.Foo<0)buf.WriteString(`,"Bar":`)fflib.WriteJsonString(buf,string(j.Bar))buf.WriteString(`,"Baz":`)fflib.AppendFloat(buf,float64(j.Baz),'g',-1,64)buf.WriteByte('}')returnnil}

现在比较一下标准库和 ffjson(利用了 ffjson.Pool())的性能差异:

standardlib:nametime/opJsonMarshall-4500ns±2%JsonUnmarshal-4677ns±2%namealloc/opJsonMarshall-448.0B±0%JsonUnmarshal-4320B±0%ffjson:nametime/opJsonMarshallFF-4538ns±1%JsonUnmarshalFF-4827ns±3%namealloc/opJsonMarshallFF-4176B±0%JsonUnmarshalFF-4448B±0%

对付 json 序列化/反序列化,标准库与 ffjson 比较反而更加高效一些。

对付内存利用情形(堆分配),可以通过 go run -gcflags="-m" 命令进行测试:

:46:19:bufescapestoheap:48:23:bufescapestoheap:27:26:&bufescapestoheap:22:6:movedtoheap:buf

easyjson[9]库也利用了和 ffjson 同样的策略,以下为基准测试结果:

standardlib:nametime/opJsonMarshall-4500ns±2%JsonUnmarshal-4677ns±2%namealloc/opJsonMarshall-448.0B±0%JsonUnmarshal-4320B±0%easyjson:nametime/opJsonMarshallEJ-4349ns±1%JsonUnmarshalEJ-4341ns±5%namealloc/opJsonMarshallEJ-4240B±0%JsonUnmarshalEJ-4256B±0%

这次,easyjson 比标准库更高效些,对付 json 序列化有 30%的性能提升,对付 json 反序列化性能提升靠近 2 倍。
通过阅读 easyjson.Marshal 的源码,可以创造它高效的缘故原由:

funcMarshal(vMarshaler)([]byte,error){w:=jwriter.Writer{}v.MarshalEasyJSON(&w)returnw.BuildBytes()}

通过 easyjson 的命令行工具天生的编码器 MarshalEasyJSON 方法可用于 json 序列化:

funceasyjson42239ddeEncode(outjwriter.Writer,inJSON){out.RawByte('{')first:=true_=first{constprefixstring=",\"Foo\":"iffirst{first=falseout.RawString(prefix[1:])}else{out.RawString(prefix)}out.Int(int(in.Foo))}{constprefixstring=",\"Bar\":"iffirst{first=falseout.RawString(prefix[1:])}else{out.RawString(prefix)}out.String(string(in.Bar))}{constprefixstring=",\"Baz\":"iffirst{first=falseout.RawString(prefix[1:])}else{out.RawString(prefix)}out.Float64(float64(in.Baz))}out.RawByte('}')}func(vJSON)MarshalEasyJSON(wjwriter.Writer){easyjson42239ddeEncode(w,v)}

正如我们所见,这里没有利用反射。
整体流程也很大略。
而且,easyjson 也可以兼容标准库:

func(vJSON)MarshalJSON()([]byte,error){w:=jwriter.Writer{}easyjson42239ddeEncodeGithubComMyCRMTeamEncodingJsonEasyjson(&w,v)returnw.Buffer.BuildBytes(),w.Error}

然而,利用这种兼容标准库的办法进行序列化会比直策应用标准库性能更差,由于在进行 json 序列化的过程中,标准库依然会通过反射布局 encoder,且 MarshalJSON 中这一段代码也会被实行。

结论

无论在标准库上做多少努力,它都不会比通过对明确的 json 构造天生 encoder/decoder的办法性能好。
而通过构造天生解析器代码的办法须要天生和掩护此代码,并且依赖于外部的库。

在做出利用第三方序列化库更换标准库的决定前,最好先测试下 json 序列化和反序列化是否是运用的性能瓶颈点,提高 json 序列化的效率是否能改进运用的性能。
如果 json 序列化和反序列化并不是运用的性能瓶颈点,为了极少的性能提升,付出第三方库的掩护本钱是不值得的。
毕竟,在大多数业务场景下,Go 的标准库 encoding/json 已经足够高效了。

via: https://medium.com/a-journey-with-go/go-is-the-encoding-json-package-really-slow-62b64d54b148

作者:Vincent Blanchon[10]译者:beiping96[11]校正:polaris1119[12]

本文由 GCTT[13] 原创编译,Go 中文网[14] 名誉推出

参考资料

[1]

easyjson: https://github.com/mailru/easyjson

[2]

jsoniter: https://github.com/json-iterator/go

[3]

ffjson: https://github.com/pquerna/ffjson

[4]

测试结果: https://gist.github.com/blanchonvincent/227b6691777a1de254ce75b304a36277

[5]

已经存在了: https://github.com/golang/go/commit/c0547476f342665514904cf2581a62135d2366c3#diff-e79d4db81e8544657cb631be813f89b4

[6]

Go 源码: https://github.com/golang/go

[7]

sync 包: https://golang.org/pkg/sync/

[8]

ffjson: https://github.com/pquerna/ffjson

[9]

easyjson: https://github.com/mailru/easyjson

[10]

Vincent Blanchon: https://medium.com/@blanchon.vincent

[11]

beiping96: https://github.com/beiping96

[12]

polaris1119: https://github.com/polaris1119

[13]

GCTT: https://github.com/studygolang/GCTT

[14]

Go 中文网: https://studygolang.com/

标签:

相关文章

光学成长与社会进步_光学_激光

人工微构造和介不雅观物理国家重点实验室一、前 言人从降生开始,光就伴随其生平。宇宙的发展与光的发展紧密联系在一起。光学的发展过程是...

智能 2025-01-22 阅读0 评论0