与Go的encoding/json兼容的快速JSON编码器/解码器
<img width="400px" src="https://yellow-cdn.veclightyear.com/2b54e442/ce2c9b91-3727-4987-ba2b-ec9e6e8563f0.png"></img>
* 版本 (预计发布日期)
* v0.9.0
|
| 在保持与encoding/json兼容的同时,我们将添加便捷的API
|
v
* v1.0.0
我们正在接受v0.9.0和v1.0.0之间将实现的功能请求。 如果您需要某个API,请在这里提交问题。
encoding/jsonMarshalJSON或UnmarshalJSONgo get github.com/goccy/go-json
将导入语句从encoding/json替换为github.com/goccy/go-json
-import "encoding/json"
+import "github.com/goccy/go-json"
| 名称 | 编码器 | 解码器 | 与encoding/json兼容 |
|---|---|---|---|
| encoding/json | 是 | 是 | 不适用 |
| json-iterator/go | 是 | 是 | 部分 |
| easyjson | 是 | 是 | 否 |
| gojay | 是 | 是 | 否 |
| segmentio/encoding/json | 是 | 是 | 部分 |
| jettison | 是 | 否 | 否 |
| simdjson-go | 否 | 是 | 否 |
| goccy/go-json | 是 | 是 | 是 |
json-iterator/go在很多方面与encoding/json不兼容(例如 https://github.com/json-iterator/go/issues/229 ),但它已经很久没有得到支持了。segmentio/encoding/json对编码器支持得很好,但对于一些解码器API如Token(流式解码)不支持我尝试进行基准测试,但没有成功。 此外,它似乎在收到意外值时会发生panic,因为没有错误处理...
基准测试结果非常慢。 似乎假定用户会正确使用缓冲池。 而且,开发似乎已经停止了
$ cd benchmarks
$ go test -bench .
<img width="700px" src="https://yellow-cdn.veclightyear.com/2b54e442/21682ed5-0ccb-44ae-9015-b3aebf668cae.png"></img> <img width="700px" src="https://yellow-cdn.veclightyear.com/2b54e442/ed06545a-ef58-44d1-9941-d3ca229bd4ce.png"></img>
go-json-fuzz是用于模糊测试的仓库。 如果您在该仓库中运行测试并发现bug,请将测试用例提交到go-json-fuzz,并在go-json中报告问题。
go-json在编码和解码方面都比其他库快得多。
通过使用自动代码生成来提高性能或使用专用接口可以更容易实现,但go-json敢于坚持与encoding/json的兼容性和简单接口。尽管如此,我们的开发目标是成为最快的库。
在这里,我们解释了go-json实现的各种加速技术。
这里列出的技术是上面列出的大多数库使用的技术。
由于json.Marshal(interface{}) ([]byte, error)结果所需的唯一值是[]byte,因此在编码过程中必须分配的唯一值是返回值[]byte。
此外,随着分配次数的增加,性能会受到影响,因此在创建[]byte时应尽可能减少分配次数。
因此,有一种技术是通过使用sync.Pool重用先前编码使用的缓冲区来减少必须分配新缓冲区的次数。
最后,您分配一个与结果缓冲区一样长的缓冲区并将内容复制到其中,理论上只需要分配一次缓冲区。
type buffer struct { data []byte } var bufPool = sync.Pool{ New: func() interface{} { return &buffer{data: make([]byte, 0, 1024)} }, } buf := bufPool.Get().(*buffer) data := encode(buf.data) // 重用 buf.data newBuf := make([]byte, len(data)) copy(newBuf, buf) buf.data = data bufPool.Put(buf)
众所周知,反射操作非常慢。
因此,利用每个二进制文件中存储类型信息的地址位置是固定的这一事实(我们称之为typeptr),
我们可以使用类型信息中的地址调用预先构建的优化过程。
例如,您可以从interface{}获取类型信息的地址,如下所示,并使用该信息调用没有反射的进程。
要在没有反射的情况下处理,传递一个指向存储值的指针(unsafe.Pointer)。
type emptyInterface struct { typ unsafe.Pointer ptr unsafe.Pointer } var typeToEncoder = map[uintptr]func(unsafe.Pointer)([]byte, error){} func Marshal(v interface{}) ([]byte, error) { iface := (*emptyInterface)(unsafe.Pointer(&v) typeptr := uintptr(iface.typ) if enc, exists := typeToEncoder[typeptr]; exists { return enc(iface.ptr) } ... }
※ 实际上,typeToEncoder可以被多个goroutine引用,因此需要进行排他控制。
Marshal的参数json.Marshal和json.Unmarshal接收interface{}值,并动 态执行类型确定以进行处理。
在正常情况下,您需要使用reflect库来动态确定类型,但由于reflect.Type被定义为interface,当您调用reflect.Type的方法时,反射的参数被转义。
因此,Marshal和Unmarshal的参数总是被转义到堆上。
然而,go-json可以使用reflect.Type的特性同时避免转义。
reflect.Type被定义为interface,但实际上reflect.Type只由reflect包中定义的结构rtype实现。
因此,到目前为止,reflect.Type与*reflect.rtype相同。
因此,通过直接处理reflect.Type的实现*reflect.rtype,可以避免转义,因为它从interface变为使用struct。
go-json直接处理*reflect.rtype的技术在rtype.go中实现
此外,同样的技术被作为一个库提取出来( https://github.com/goccy/go-reflect )
最初,这个特性是go-json的默认行为。
但经过仔细测试后,我发现如果将大值传递给json.Marshal(),并且参数无法分配到栈上,它就无法正确地转义到堆上(Go编译器的一个bug)。
因此,在解决这个问题之前,这个特性将作为可选提供。
要使用它,请添加NoEscape,如MarshalNoEscape()
我解释过,您可以使用typeptr从类型信息调用预先构建的过程。
在其他库中,这个专用过程通过将其作为匿名函数等进行函数调用来处理,但函数调用本质上是缓慢的过程,应尽可能避免。