与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/json
MarshalJSON
或UnmarshalJSON
go 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
从类型信息调用预先构建的过程。
在其他库中,这个专用过程通过将其作为匿名函数等进行函数调用来处理,但函数调用本质上是缓慢的过程,应尽可能避免。