go-json

go-json

Go语言高性能JSON编解码库 兼容标准库

go-json是一个高性能JSON编解码库,完全兼容Go标准库encoding/json。它采用多项优化技术提升性能,如缓冲区重用、反射消除和指令序列编码。该库还支持自定义选项、彩色输出和动态过滤结构体字段。go-json为Go开发者提供了处理JSON数据的高效替代方案。

go-jsonJSON编码器JSON解码器性能优化encoding/jsonGithub开源项目

go-json

Go GoDoc codecov

与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
  • 快速(参见基准测试部分
  • 可通过选项灵活定制
  • 可对编码的字符串进行着色
  • 可将context.Context传播到MarshalJSONUnmarshalJSON
  • 可以动态地以类型安全的方式过滤结构体的字段

安装

go get github.com/goccy/go-json

使用方法

将导入语句从encoding/json替换为github.com/goccy/go-json

-import "encoding/json"
+import "github.com/goccy/go-json"

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>

解码

<img width="700" alt="" src="https://yellow-cdn.veclightyear.com/2b54e442/0c79eb10-7638-4359-ba5b-109e9706ff37.png"> <img width="700" alt="" src="https://yellow-cdn.veclightyear.com/2b54e442/a0027507-2b0d-4167-8637-70fc0ed98908.png"> <img width="700" alt="" src="https://yellow-cdn.veclightyear.com/2b54e442/d37d8213-1129-439c-88bb-9cc59840ca3f.png">

模糊测试

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.Marshaljson.Unmarshal接收interface{}值,并动态执行类型确定以进行处理。 在正常情况下,您需要使用reflect库来动态确定类型,但由于reflect.Type被定义为interface,当您调用reflect.Type的方法时,反射的参数被转义。

因此,MarshalUnmarshal的参数总是被转义到堆上。 然而,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从类型信息调用预先构建的过程。

在其他库中,这个专用过程通过将其作为匿名函数等进行函数调用来处理,但函数调用本质上是缓慢的过程,应尽可能避免。

因此,go-json采用了基于指令的执行处理系统,这也用于实现编程语言的虚拟机。

如果是第一次编码的类型,创建编码所需的操作码(指令)序列。 从第二次开始,使用typeptr获取缓存的预构建操作码序列并基于它进行编码。操作码序列的示例如下所示。

json.Marshal(struct{ X int `json:"x"` Y string `json:"y"` }{X: 1, Y: "hello"})

当编码像上面这样的结构时,创建像这样的操作码序列:

- opStructFieldHead ( `{` )
- opStructFieldInt ( `"x": 1,` )
- opStructFieldString ( `"y": "hello"` )
- opStructEnd ( `}` )
- opEnd

※ 处理每个操作时,写入右侧的字符。

此外,每个操作码由以下结构管理(伪代码)。

type opType int const ( opStructFieldHead opType = iota opStructFieldInt opStructFieldStirng opStructEnd opEnd ) type opcode struct { op opType key []byte next *opcode }

使用操作码序列进行编码的过程大致实现如下。

func encode(code *opcode, b []byte, p unsafe.Pointer) ([]byte, error) { for { switch code.op { case opStructFieldHead: b = append(b, '{') code = code.next case opStructFieldInt: b = append(b, code.key...) b = appendInt((*int)(unsafe.Pointer(uintptr(p)+code.offset))) code = code.next case opStructFieldString: b = append(b, code.key...) b = appendString((*string)(unsafe.Pointer(uintptr(p)+code.offset))) code = code.next case opStructEnd: b = append(b, '}') code = code.next case opEnd: goto END } } END: return b, nil }

通过这种方式,使用巨大的switch-case通过操作链表操作码来进行编码,以避免不必要的函数调用。

操作码序列优化

使用操作码序列进行编码的优点之一是易于优化。 上面提到的操作码序列实际上被转换为以下优化的操作并使用。

- opStructFieldHeadInt ( `{"x": 1,` )
- opStructEndString ( `"y": "hello"}` )
- opEnd

它已经从5个操作码减少到3个操作码! 减少操作码的数量意味着减少switch-case的分支数。 换句话说,操作数越接近1,处理速度就越快。

go-json中,像上面这样优化以减少操作码本身的数量,并通过准备具有优化路径的操作码来加速。

将递归调用从CALL更改为JMP

如果类型被递归定义如下,则在编码期间需要递归处理:

type T struct { X int U *U } type U struct { T *T } b, err := json.Marshal(&T{ X: 1, U: &U{ T: &T{ X: 2, }, }, }) fmt.Println(string(b)) 然而,如果类型信息过多,会占用大量内存,所以默认情况下我们只在切片大小在**2Mib**内时使用这种优化。 如果这种方法不可用,它将回退到上述基于`atomic`的处理。 如果你想了解更多,请参考[这里](https://github.com/goccy/go-json/blob/master/internal/runtime/type.go#L36-L100)的实现。 ## 解码器 ### 通过typeptr从map分派到切片 与编码器一样,解码器也使用typeptr调用专用处理。 ### 使用NUL字符进行更快的终止字符检查 为了解码,你必须按位置遍历输入缓冲区。此时,如果检查缓冲区是否已到达末尾,会非常慢。 `buf``[]byte`类型变量。保存传递给解码器的字符串 `cursor``int64`类型变量。保存当前读取位置 ```go buflen := len(buf) for ; cursor < buflen; cursor++ { // 始终比较cursor和buflen,这很慢。 switch buf[cursor] { case ' ', '\n', '\r', '\t': } }

因此,通过在读取缓冲区末尾添加NUL\000)字符,可以同时检查终止字符和其他字符。

for { switch buf[cursor] { case ' ', '\n', '\r', '\t': case '\000': return nil } cursor++ }

使用边界检查消除

由于NUL字符优化,Go编译器每次都会进行边界检查,尽管buf[cursor]不会导致越界访问。

因此,go-json通过指针操作获取热点字符来消除边界检查。例如,以下代码:

func char(ptr unsafe.Pointer, offset int64) byte { return *(*byte)(unsafe.Pointer(uintptr(ptr) + uintptr(offset))) } p := (*sliceHeader)(&unsafe.Pointer(buf)).data for { switch char(p, cursor) { case ' ', '\n', '\r', '\t': case '\000': return nil } cursor++ }

使用位图检查结构体字段的存在

我通过性能分析结果发现,在结构体解码中,字段查找过程耗时很长。

例如,考虑将像{"a":1,"b":2,"c":3}这样的字符串解码到以下结构:

type T struct { A int `json:"a"` B int `json:"b"` C int `json:"c"` }

此时,发现在解码过程中,从字段名获取对应的解码过程如下所示,这需要大量时间:

fieldName := decodeKey(buf, cursor) // "a"或"b"或"c" decoder, exists := fieldToDecoderMap[fieldName] // 非常慢 if exists { decoder(buf, cursor) } else { skipValue(buf, cursor) }

为了改进这个过程,json-iterator/go进行了优化,当结构体的字段数不超过10个时,可以通过switch-case分支(switch-case比map快)。然而,由于使用FNV算法哈希的值进行条件分支,存在哈希冲突的风险。另外,gojay通过让库用户自己编写switch-case来高速处理这部分。

go-json考虑并实现了一种不同于这些的新方法。我称之为位图字段优化

每个字符的值范围可以用[256]byte表示。此外,如果结构体的字段数不超过8个,int8类型可以表示每个字段的状态。 换句话说,它具有以下结构:

  • 基础(8位):00000000
  • 键"a":00000001(将键"a"分配给第一位)
  • 键"b":00000010(将键"b"分配给第二位)
  • 键"c":00000100(将键"c"分配给第三位)

位图结构如下:

        | 键索引(0) |
------------------------
 0      | 00000000     |
 1      | 00000000     |
~~      |              |
97 (a)  | 00000001     |
98 (b)  | 00000010     |
99 (c)  | 00000100     |
~~      |              |
255     | 00000000     |

你可以将其视为高度为256、宽度为字段名中最大字符串长度的位图。 换句话说,可以用以下类型表示:

[maxFieldKeyLength][256]int8

解码字段字符时,通过参考预先构建的位图来检查相应字符是否存在,如下所示:

var curBit int8 = math.MaxInt8 // 11111111 c := char(buf, cursor) bit := bitmap[keyIdx][c] curBit &= bit if curBit == 0 { // 未找到字段 }

如果直到字段字符串结束curBit都不为0,那么该字符串可能命中了其中一个字段。 但是,如果解码的字符串比字段字符串短,可能会出现误判。

  • 输入:{"a":1}
type T struct { X int `json:"abc"` }

※ 由于aabc短,可以在字段字符结束前解码而不使curBit为0。

不用担心。在这种情况下,通过比较a的字符串长度与abc的字符串长度,你可以判断是否命中,所以没有问题。

最后,计算设置为1的位的位置并获取相应的值,就完成了。

使用这种技术,只需位操作和对切片的访问就可以进行字段查找。

go-json对9个及以上、16个及以下的字段使用类似的技术。此时,位图构造为[maxKeyLen][256]int16类型。

目前,除了字段数量的限制外,当字段名的最大长度较长(具体来说,64字节或更多)时,出于节省内存使用的考虑,不会执行此优化。

其他

我还进行了许多其他优化。我会找时间写about它们。如果你对这里写的内容或其他优化有任何问题,请访问gophers.slack.com上的#go-json频道。

参考

关于go-json的故事,以下文章仅有日语版本:

寻找赞助商

我正在为这个库寻找赞助商。这个库是我在业余时间作为个人项目开发的。如果你在项目中使用这个库时需要快速响应或问题解决,请注册成为赞助商。我将尽可能合作。当然,这个库是以MIT许可证开发的,所以你可以免费自由使用它。

许可证

MIT

编辑推荐精选

蛙蛙写作

蛙蛙写作

AI小说写作助手,一站式润色、改写、扩写

蛙蛙写作—国内先进的AI写作平台,涵盖小说、学术、社交媒体等多场景。提供续写、改写、润色等功能,助力创作者高效优化写作流程。界面简洁,功能全面,适合各类写作者提升内容品质和工作效率。

AI辅助写作AI工具蛙蛙写作AI写作工具学术助手办公助手营销助手AI助手
Trae

Trae

字节跳动发布的AI编程神器IDE

Trae是一种自适应的集成开发环境(IDE),通过自动化和多元协作改变开发流程。利用Trae,团队能够更快速、精确地编写和部署代码,从而提高编程效率和项目交付速度。Trae具备上下文感知和代码自动完成功能,是提升开发效率的理想工具。

AI工具TraeAI IDE协作生产力转型热门
问小白

问小白

全能AI智能助手,随时解答生活与工作的多样问题

问小白,由元石科技研发的AI智能助手,快速准确地解答各种生活和工作问题,包括但不限于搜索、规划和社交互动,帮助用户在日常生活中提高效率,轻松管理个人事务。

热门AI助手AI对话AI工具聊天机器人
Transly

Transly

实时语音翻译/同声传译工具

Transly是一个多场景的AI大语言模型驱动的同声传译、专业翻译助手,它拥有超精准的音频识别翻译能力,几乎零延迟的使用体验和支持多国语言可以让你带它走遍全球,无论你是留学生、商务人士、韩剧美剧爱好者,还是出国游玩、多国会议、跨国追星等等,都可以满足你所有需要同传的场景需求,线上线下通用,扫除语言障碍,让全世界的语言交流不再有国界。

讯飞智文

讯飞智文

一键生成PPT和Word,让学习生活更轻松

讯飞智文是一个利用 AI 技术的项目,能够帮助用户生成 PPT 以及各类文档。无论是商业领域的市场分析报告、年度目标制定,还是学生群体的职业生涯规划、实习避坑指南,亦或是活动策划、旅游攻略等内容,它都能提供支持,帮助用户精准表达,轻松呈现各种信息。

AI办公办公工具AI工具讯飞智文AI在线生成PPTAI撰写助手多语种文档生成AI自动配图热门
讯飞星火

讯飞星火

深度推理能力全新升级,全面对标OpenAI o1

科大讯飞的星火大模型,支持语言理解、知识问答和文本创作等多功能,适用于多种文件和业务场景,提升办公和日常生活的效率。讯飞星火是一个提供丰富智能服务的平台,涵盖科技资讯、图像创作、写作辅助、编程解答、科研文献解读等功能,能为不同需求的用户提供便捷高效的帮助,助力用户轻松获取信息、解决问题,满足多样化使用场景。

热门AI开发模型训练AI工具讯飞星火大模型智能问答内容创作多语种支持智慧生活
Spark-TTS

Spark-TTS

一种基于大语言模型的高效单流解耦语音令牌文本到语音合成模型

Spark-TTS 是一个基于 PyTorch 的开源文本到语音合成项目,由多个知名机构联合参与。该项目提供了高效的 LLM(大语言模型)驱动的语音合成方案,支持语音克隆和语音创建功能,可通过命令行界面(CLI)和 Web UI 两种方式使用。用户可以根据需求调整语音的性别、音高、速度等参数,生成高质量的语音。该项目适用于多种场景,如有声读物制作、智能语音助手开发等。

咔片PPT

咔片PPT

AI助力,做PPT更简单!

咔片是一款轻量化在线演示设计工具,借助 AI 技术,实现从内容生成到智能设计的一站式 PPT 制作服务。支持多种文档格式导入生成 PPT,提供海量模板、智能美化、素材替换等功能,适用于销售、教师、学生等各类人群,能高效制作出高品质 PPT,满足不同场景演示需求。

讯飞绘文

讯飞绘文

选题、配图、成文,一站式创作,让内容运营更高效

讯飞绘文,一个AI集成平台,支持写作、选题、配图、排版和发布。高效生成适用于各类媒体的定制内容,加速品牌传播,提升内容营销效果。

热门AI辅助写作AI工具讯飞绘文内容运营AI创作个性化文章多平台分发AI助手
材料星

材料星

专业的AI公文写作平台,公文写作神器

AI 材料星,专业的 AI 公文写作辅助平台,为体制内工作人员提供高效的公文写作解决方案。拥有海量公文文库、9 大核心 AI 功能,支持 30 + 文稿类型生成,助力快速完成领导讲话、工作总结、述职报告等材料,提升办公效率,是体制打工人的得力写作神器。

下拉加载更多