Sonic是一款由字節跳動開發的一個全新的高性能、適用廣泛的 JSON 庫。在設計上借鑒了多款JSON庫,同時為了實現對標準庫的真正插拔式替換,Sonic使用了 JIT[1] (即時編譯) 。
介紹
我們在日常開發中,常常會對JSON進行序列化和反序列化。Golang提供了encoding/json包對JSON進行Marshal/Unmarshal操作。但是在大規模數據場景下,該包的性能和開銷確實會有點不夠看。在生產環境下,JSON 序列化和反序列化會被頻繁的使用到。在測試中,CPU使用率接近 10%,其中極端情況下超過 40%。因此,JSON 庫的性能是提高機器利用率的關鍵問題。
Sonic是一款由字節跳動開發的一個全新的高性能、適用廣泛的 JSON 庫。在設計上借鑒了多款JSON庫,同時為了實現對標準庫的真正插拔式替換,Sonic使用了 JIT[1] (即時編譯) 。
Sonic的特色
我們可以看出:Sonic是一個主打快的JSON庫。
- 運行時對象綁定,無需代碼生成
- 完備的 JSON 操作 API
- 快,更快,還要更快!
Sonic的設計
- 針對編解碼動態匯編的函數調用開銷,使用 JIT 技術在運行時組裝與模式對應的字節碼(匯編指令) ,最終將其以 Golang 函數的形式緩存在堆外內存上。
- 針對大數據和小數據共存的實際場景,使用預處理判斷(字符串大小、浮點數精度等)將 SIMD 與標量指令相結合,從而實現對實際情況的最佳適應。
- 對于 Golang 語言編譯優化的不足,使用 C/Clang 編寫和編譯核心計算函數,并且開發了一套 asm2asm[2] 工具,將經過充分優化的 x86 匯編代碼轉換為 Plan9 格式,最終加載到 Golang 運行時中。
- 考慮到解析和跳過解析之間的速度差異很大, 惰性加載機制當然也在 AST 解析器中使用了,但以一種更具適應性和高效性的方式來降低多鍵查詢的開銷。
在細節上,Sonic進行了一些進一步的優化:
- 由于 Golang 中的原生匯編函數不能被內聯,發現其成本甚至超過了 C 編譯器的優化所帶來的改善。所以在 JIT 中重新實現了一組輕量級的函數調用:
全局函數表+靜態偏移量,用于調用指令
使用寄存器傳遞參數
- Sync.Map 一開始被用來緩存編解碼器,但是對于準靜態(讀遠多于寫),元素較少(通常不足幾十個)的場景,它的性能并不理想,所以使用開放尋址哈希和 RCU 技術重新實現了一個高性能且并發安全的緩存。
安裝使用
當前我使用的go version是1.21。
官方建議的版本:
- Go 1.16~1.21
- linux / macOS / windows(需要 Go1.17 以上)
- Amd64 架構
# 下載sonic依賴
$ go get Github.com/bytedance/sonic
基本使用
sonic提供了許多功能。本文僅列舉其中較為有特色的功能。感興趣的同學可以去看一下官方的examples
序列化/反序列化
sonic的使用類似于標準包encoding/json包的使用.
func base() {
m := map[string]interface{}{
"name": "z3",
"age": 20,
}
// sonic序列化
byt, err := sonic.Marshal(&m)
if err != nil {
log.Println(err)
}
fmt.Printf("json: %+vn", string(byt))
// sonic反序列化
um := make(map[string]interface{})
err = sonic.Unmarshal(byt, &um)
if err != nil {
log.Println(err)
}
fmt.Printf("unjson: %+vn", um)
}
// print
// json: {"name":"z3","age":20}
// unjson: map[age:20 name:z3]
sonic還支持流式的輸入輸出
Sonic 支持解碼 io.Reader 中輸入的 json,或將對象編碼為 json 后輸出至 io.Writer,以處理多個值并減少內存消耗
func base() {
m := map[string]interface{}{
"name": "z3",
"age": 20,
}
// 流式io編解碼
// 編碼
var encbuf bytes.Buffer
enc := sonic.ConfigDefault.NewEncoder(&encbuf)
if err := enc.Encode(m); err != nil {
log.Fatal(err)
} else {
fmt.Printf("cutomize encoder: %+v", encbuf.String())
}
// 解碼
var decbuf bytes.Buffer
decbuf.WriteString(encbuf.String())
clear(m)
dec := sonic.ConfigDefault.NewDecoder(&decbuf)
if err := dec.Decode(&m); err != nil {
log.Fatal(err)
} else {
fmt.Printf("cutomize decoder: %+vn", m)
}
}
// print
// cutomize encoder: {"name":"z3","age":20}
// cutomize decoder: map[age:20 name:z3]
配置
在上面的自定義流式編碼解碼器,細心的朋友可能看到我們創建編碼器和解碼器的時候,是通過sonic.ConfigDefault.NewEncoder() / sonic.ConfigDefault.NewDecoder()這兩個函數進行調用的。那么sonic.ConfigDefault是什么?
我們可以通過查看源碼:
var (
// ConfigDefault is the default config of APIs, AIming at efficiency and safty.
// ConfigDefault api的默認配置,針對效率和安全。
ConfigDefault = Config{}.Froze()
// ConfigStd is the standard config of APIs, aiming at being compatible with encoding/json.
// ConfigStd是api的標準配置,旨在與encoding/json兼容。
ConfigStd = Config{
Escapehtml : true,
SortMapKeys: true,
CompactMarshaler: true,
CopyString : true,
ValidateString : true,
}.Froze()
// ConfigFastest is the fastest config of APIs, aiming at speed.
// ConfigFastest是api的最快配置,旨在提高速度。
ConfigFastest = Config{
NoQuoteTextMarshaler: true,
}.Froze()
)
sonic提供了三種常用的Config配置。這些配置中對一些場景已經預定義好了對應的Config。
其實我們使用的sonic.Marshal()函數就是調用了默認的ConfigDefault
// Marshal returns the JSON encoding bytes of v.
func Marshal(val interface{}) ([]byte, error) {
return ConfigDefault.Marshal(val)
}
// Unmarshal parses the JSON-encoded data and stores the result in the value pointed to by v.
// NOTICE: This API copies given buffer by default,
// if you want to pass JSON more efficiently, use UnmarshalString instead.
func Unmarshal(buf []byte, val interface{}) error {
return ConfigDefault.Unmarshal(buf, val)
}
但是在一些場景下我們不滿足于sonic預定義的三個Config。此時我們可以定義自己的Config進行個性化的編碼和解碼。
首先先看一下Config的結構。
// Config is a combination of sonic/encoder.Options and sonic/decoder.Options
type Config struct {
// EscapeHTML indicates encoder to escape all HTML characters
// after serializing into JSON (see https://pkg.go.dev/encoding/json#HTMLEscape).
// WARNING: This hurts performance A LOT, USE WITH CARE.
EscapeHTML bool
// SortMapKeys indicates encoder that the keys of a map needs to be sorted
// before serializing into JSON.
// WARNING: This hurts performance A LOT, USE WITH CARE.
SortMapKeys bool
// CompactMarshaler indicates encoder that the output JSON from json.Marshaler
// is always compact and needs no validation
CompactMarshaler bool
// NoQuoteTextMarshaler indicates encoder that the output text from encoding.TextMarshaler
// is always escaped string and needs no quoting
NoQuoteTextMarshaler bool
// NoNullSliceOrMap indicates encoder that all empty Array or Object are encoded as '[]' or '{}',
// instead of 'null'
NoNullSliceOrMap bool
// UseInt64 indicates decoder to unmarshal an integer into an interface{} as an
// int64 instead of as a float64.
UseInt64 bool
// UseNumber indicates decoder to unmarshal a number into an interface{} as a
// json.Number instead of as a float64.
UseNumber bool
// UseUnicodeErrors indicates decoder to return an error when encounter invalid
// UTF-8 escape sequences.
UseUnicodeErrors bool
// DisallowUnknownFields indicates decoder to return an error when the destination
// is a struct and the input contains object keys which do not match any
// non-ignored, exported fields in the destination.
DisallowUnknownFields bool
// CopyString indicates decoder to decode string values by copying instead of referring.
CopyString bool
// ValidateString indicates decoder and encoder to valid string values: decoder will return errors
// when unescaped control chars(u0000-u001f) in the string value of JSON.
ValidateString bool
}
由于字段較多。筆者就選擇幾個字段進行演示,其他字段使用方式都是一致。
假設我們希望對JSON序列化按照key進行排序以及將JSON編碼成緊湊的格式。我們可以配置Config進行Marshal操作
func base() {
snc := sonic.Config{
CompactMarshaler: true,
SortMapKeys: true,
}.Froze()
snc.Marshal(obj)
}
考慮到排序帶來的性能損失(約 10% ), sonic 默認不會啟用這個功能。
Sonic 默認將基本類型( struct , map 等)編碼為緊湊格式的 JSON ,除非使用 json.RawMessage or json.Marshaler 進行編碼:sonic 確保輸出的 JSON 合法,但出于性能考慮,不會加工成緊湊格式。我們提供選項 encoder.CompactMarshaler 來添加此過程,
Ast.Node
sonic提供了Ast.Node的功能。Sonic/ast.Node 是完全獨立的 JSON 抽象語法樹庫。它實現了序列化和反序列化,并提供了獲取和修改通用數據的魯棒的 API。
先來簡單介紹一下Ast.Node:ast.Node 通常指的是編程語言中的抽象語法樹(Abstract Syntax Tree)節點。抽象語法樹是編程語言代碼在編譯器中的內部表示,它以樹狀結構展現代碼的語法結構,便于編譯器進行語法分析、語義分析、優化等操作。
在很多編程語言的編譯器或解釋器實現中,抽象語法樹中的每個元素(節點)都會有對應的數據結構表示,通常這些數據結構會被稱為 ast.Node 或類似的名字。每個 ast.Node 表示源代碼中的一個語法結構,如表達式、語句、函數聲明等。
抽象語法樹的節點可以包含以下信息:
- 節點的類型:例如表達式、語句、函數調用等。
- 節點的內容:節點所代表的源代碼的內容。
- 子節點:一些節點可能包含子節點,這些子節點也是抽象語法樹的節點,用于構建更復雜的語法結構。
- 屬性:一些節點可能會包含附加的屬性,如變量名、操作符類型等。
我們通過幾個案例理解一下Ast.Node的使用。
準備數據
data := `{"name": "z3","info":{"num": [11,22,33]}}`
將數據轉換為Ast.Node
通過傳入bytes或者string返回一個Ast.Node。其中你可以指定path獲取JSON中的子路徑元素。
每個路徑參數必須是整數或者字符串
- 整數是目標索引(>=0),表示以數組形式搜索當前節點。
- 字符串為目標key,表示搜索當前節點為對象。
// 函數簽名:
func Get(src []byte, path ...interface{}) (ast.Node, error) {
return GetFromString(string(src), path...)
}
// GetFromString與Get相同,只是src是字符串,這樣可以減少不必要的內存拷貝。
func GetFromString(src string, path ...interface{}) (ast.Node, error) {
return ast.NewSearcher(src).GetByPath(path...)
}
獲取當前節點的完整數據
func base() {
data := `{"name": "z3","info":{"num": [11,22,33]}}`
// no path return all json string
root, err := sonic.GetFromString(data)
if err != nil {
log.Panic(err)
}
if raw, err := root.Raw(); err != nil {
log.Panic(err)
} else {
log.Println(raw)
}
}
// print
// 2023/08/26 17:15:52 {"name": "z3","info":{"num": [11,22,33]}}
根據path或者索引獲取數據
func base() {
data := `{"name": "z3","info":{"num": [11,22,33]}}`
// no path return all json string
root, err := sonic.GetFromString(data)
if err != nil {
log.Panic(err)
}
// according to path(根據key,查詢當前node下的元素)
if path, err := root.GetByPath("name").Raw(); err != nil {
log.Panic(err)
} else {
log.Println(path)
}
// indexOrget (同時提供index和key進行索引和key的匹配)
if path, err := root.IndexOrGet(1, "info").Raw(); err != nil {
log.Panic(err)
} else {
log.Println(path)
}
// index (按照index進行查找當前node下的元素)
// root.Index(1).Index(0).Raw()意味著
// root.Index(1) == "info"
// root.Index(1).Index(0) == "num"
// root.Index(1).Index(0).Raw() == "[11,22,33]"
if path, err := root.Index(1).Index(0).Raw(); err != nil {
log.Panic(err)
} else {
log.Println(path)
}
}
// print
// 2023/08/26 17:17:49 "z3"
// 2023/08/26 17:17:49 {"num": [11,22,33]}
// 2023/08/26 17:17:49 [11,22,33]
Ast.Node支持鏈式調用。故我們可以從root node節點,根據path路徑向下搜索指定的元素。
index和key混用
user := root.GetByPath("statuses", 3, "user") // === root.Get("status").Index(3).Get("user")
根據path進行修改數據
func base() {
data := `{"name": "z3","info":{"num": [11,22,33]}}`
// no path return all json string
root, err := sonic.GetFromString(data)
if err != nil {
log.Panic(err)
}
// according to path
if path, err := root.GetByPath("name").Raw(); err != nil {
log.Panic(err)
} else {
log.Println(path)
}
// indexOrget (同時提供index和key進行索引和key的匹配)
if path, err := root.IndexOrGet(1, "info").Raw(); err != nil {
log.Panic(err)
} else {
log.Println(path)
}
// index
if path, err := root.Index(1).Index(0).Raw(); err != nil {
log.Panic(err)
} else {
log.Println(path)
}
// set
// ast提供了很多go類型轉換node的函數
if _, err := root.Index(1).SetByIndex(0, ast.NewArray([]ast.Node{
ast.NewNumber("101"),
ast.NewNumber("202"),
})); err != nil {
log.Panic(err)
}
raw, _ := root.Raw()
log.Println(raw)
}
// print
// 2023/08/26 17:23:55 "z3"
// 2023/08/26 17:23:55 {"num": [11,22,33]}
// 2023/08/26 17:23:55 [11,22,33]
// 2023/08/26 17:23:55 {"name":"z3","info":{"num":[101,202]}}
序列化
func base() {
data := `{"name": "z3","info":{"num": [11,22,33]}}`
// no path return all json string
root, err := sonic.GetFromString(data)
if err != nil {
log.Panic(err)
}
bts, _ := root.MarshalJSON()
log.Println("Ast.Node(Marshal): ", string(bts))
btes, _ := json.Marshal(&root)
log.Println("encoding/json (Marshal): ", string(btes))
}
// print
// 2023/08/26 17:39:06 Ast.Node(Marshal): {"name": "z3","info":{"num": [11,22,33]}}
// 2023/08/26 17:39:06 encoding/json (Marshal): {"name":"z3","info":{"num":[11,22,33]}}
?: 使用json.Marshal() (必須傳遞指向節點的指針)
API
Ast.Node提供了許多有特色的API,感興趣的朋友可以去試一下。
- 合法性檢查:Check(), Error(), Valid(), Exist()
- 索引:Index(), Get(), IndexPair(), IndexOrGet(), GetByPath()
- 轉換至 go 內置類型:Int64(), Float64(), String(), Number(), Bool(), Map[UseNumber|UseNode](), Array[UseNumber|UseNode](), Interface[UseNumber|UseNode]()
- go 類型打包:NewRaw(), NewNumber(), NewNull(), NewBool(), NewString(), NewObject(), NewArray()
- 迭代:Values(), Properties(), ForEach(), SortKeys()
- 修改:Set(), SetByIndex(), Add()
最佳實踐
預熱
由于 Sonic 使用 golang-asm 作為 JIT 匯編器,這個庫并不適用于運行時編譯,第一次運行一個大型模式可能會導致請求超時甚至進程內存溢出。為了更好地穩定性,我們建議在運行大型模式或在內存有限的應用中,在使用 Marshal()/Unmarshal() 前運行 Pretouch()。
拷貝字符串
當解碼 沒有轉義字符的字符串時, sonic 會從原始的 JSON 緩沖區內引用而不是復制到新的一個緩沖區中。這對 CPU 的性能方面很有幫助,但是可能因此在解碼后對象仍在使用的時候將整個 JSON 緩沖區保留在內存中。實踐中我們發現,通過引用 JSON 緩沖區引入的額外內存通常是解碼后對象的 20% 至 80% ,一旦應用長期保留這些對象(如緩存以備重用),服務器所使用的內存可能會增加。我們提供了選項 decoder.CopyString() 供用戶選擇,不引用 JSON 緩沖區。這可能在一定程度上降低 CPU 性能
func base() {
// 在sonic.Config中進行配置
snc := sonic.Config{
CopyString: true,
}.Froze()
}
傳遞字符串還是字節數組
為了和 encoding/json 保持一致,我們提供了傳遞 []byte 作為參數的 API ,但考慮到安全性,字符串到字節的復制是同時進行的,這在原始 JSON 非常大時可能會導致性能損失。因此,你可以使用 UnmarshalString() 和 GetFromString() 來傳遞字符串,只要你的原始數據是字符串,或零拷貝類型轉換對于你的字節數組是安全的。我們也提供了 MarshalString() 的 API ,以便對編碼的 JSON 字節數組進行零拷貝類型轉換,因為 sonic 輸出的字節始終是重復并且唯一的,所以這樣是安全的。
零拷貝類型轉換是一種技術,它允許你在不進行實際數據復制的情況下,將一種數據類型轉換為另一種數據類型。這種轉換通過操作原始內存塊的指針和切片來實現,避免了額外的數據復制,從而提高性能并減少內存開銷.
需要注意的是,零拷貝類型轉換雖然可以提高性能,但也可能引入一些安全和可維護性的問題,特別是當直接操作指針或內存映射時。
性能優化
在 完全解析的場景下, Unmarshal() 表現得比 Get()+Node.Interface() 更好。
func base() {
data := `{"name": "z3","info":{"num": [11,22,33]}}`
// complete parsing
m := map[string]interface{}{}
sonic.Unmarshal([]byte(data), &m)
}
但是如果你只有特定 JSON的部分模式,你可以將 Get() 和 Unmarshal() 結合使用:
func base() {
data := `{"name": "z3","info":{"num": [11,22,33]}}`
// complete parsing
m := map[string]interface{}{}
sonic.Unmarshal([]byte(data), &m)
// partial parsing
clear(m)
node, err := sonic.GetFromString(data, "info", "num", 1)
if err != nil {
panic(err)
}
log.Println(node.Raw())
}
原文鏈接
https://juejin.cn/post/7271429136659791907
相關資料
[1]https://en.wikipedia.org/wiki/Jit: https://en.wikipedia.org%2Fwiki%2FJit
[2]https://github.com/chenzhuoyu/asm2asm: https://github.com%2Fchenzhuoyu%2Fasm2asm
本文轉載自微信公眾號「 程序員升級打怪之旅」,作者「 Shyunn&王中陽Go」