本文作者從評判標準、功能評測、性能評測等多方面剖析三方庫哪些家強,並給出了比較務實的建議。 引言 為了小伙伴理解,彙總了一下文章中會提及的術語名詞解釋,請放心品讀,歡迎一起討論交流! 你真的瞭解 Go 標準庫嗎? 問題一:標準庫可以反序列化普通的字元串嗎?執行下麵的代碼會報錯嗎? var s str ...
本文作者從評判標準、功能評測、性能評測等多方面剖析三方庫哪些家強,並給出了比較務實的建議。
引言
你真的瞭解 Go 標準庫嗎?
問題一:標準庫可以反序列化普通的字元串嗎?執行下麵的代碼會報錯嗎?
var s string
err := json.Unmarshal([]byte(`"Hello, world!"`), &s)
assert.NoError(t, err)
fmt.Println(s)
// 輸出:
// Hello, world!
解:其實標準庫解析不僅支持是對象、數組,同時也可以是字元串、數值、布爾值以及空值,但需要註意,上面字元串中的雙引號不能缺,否則將不是一個合法的 json 序列,會返回錯誤。
cert := struct {
Username string `json:"username"`
Password string `json:"password"`
}{}
err = json.Unmarshal([]byte(`{"UserName":"root","passWord":"123456"}`), &cert)
if err != nil {
fmt.Println("err =", err)
} else {
fmt.Println("username =", cert.Username)
fmt.Println("password =", cert.Password)
}
// 輸出:
// username = root
// password = 123456
解:如果遇到大小寫問題,標準庫會儘可能地進行大小寫轉換,即:一個 key 與結構體中的定義不同,但忽略大小寫後是相同的,那麼依然能夠為欄位賦值。
為什麼使用第三方庫,標準庫有哪些不足?
-
API 不夠靈活:如沒有提供按需載入機制等;
-
性能不太高:標準庫大量使用反射獲取值,首先 Go 的反射本身性能較差,較耗費 CPU 配置;其次頻繁分配對象,也會帶來記憶體分配和 GC 的開銷;
三方庫哪些家強?
-
熱門的三方庫有哪些? -
內部實現原理是什麼? -
如何結合業務去選型?
庫名 | encoder | decoder | compatible | star 數 (2023.04.19) | 社區維護性 |
StdLib(encoding/json)[2] | ✔️ | ✔️ | N/A | - | - |
FastJson(valyala/fastjson)[3] | ✔️ | ✔️ | ❌ | 1.9k | 較差 |
GJson(tidwall/gjson)[4] | ✔️ | ✔️ | ❌ | 12.1k | 較好 |
JsonParser(buger/jsonparser)[5] | ✔️ | ✔️ | ❌ | 5k | 較差 |
JsonIter(json-iterator/go)[6] | ✔️ | ✔️ | 部分相容 | 12.1k | 較差 |
GoJson(goccy/go-json)[7] | ✔️ | ✔️ | ✔️ | 2.2k | 較好 |
EasyJson(mailru/easyjson)[8] | ✔️ | ✔️ | ❌ | 4.1k | 較差 |
Sonic(bytedance/sonic)[9] | ✔️ | ✔️ | ✔️ | 4.1k | 較好 |
評判標準
評判標準包含三個維度:
-
性能:內部實現原理是什麼,是否使用反射機制;
-
穩定性:考慮到要投入生產使用,必須是一個較為穩定的三方庫;
-
功能靈活性:是否支持 Unmarshal 到 map 或 struct,是否提供的一些定製化抽取的 API;
-
泛型(generic)編解碼:json 沒有對應的 schema,只能依據自描述語義將讀取到的 value 解釋為對應語言的運行時對象,例如:json object 轉化為 Go map[string]interface{};
-
定型(binding)編解碼:json 有對應的 schema,可以同時結合模型定義(Go struct)與 json 語法,將讀取到的 value 綁定到對應的模型欄位上去,同時完成數據解析與校驗;
-
查找(get)& 修改(set):指定某種規則的查找路徑(一般是 key 與 index 的集合),獲取需要的那部分 json value 並處理。
功能評測
性能評測
-
Small[11](400B, 11 keys, 3 layers) -
Medium[12](13KB, 300+ key, 6 layers) -
Large[13](635KB, 10000+ key, 6 layers)
func BenchmarkEncoder_Generic_StdLib(b *testing.B) {
_, _ = json.Marshal(_GenericValue)
b.SetBytes(int64(len(TwitterJson)))
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = json.Marshal(_GenericValue)
}
}
func BenchmarkEncoder_Binding_StdLib(b *testing.B) {
_, _ = json.Marshal(&_BindingValue)
b.SetBytes(int64(len(TwitterJson)))
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = json.Marshal(&_BindingValue)
}
}
func BenchmarkEncoder_Parallel_Generic_StdLib(b *testing.B) {
_, _ = json.Marshal(_GenericValue)
b.SetBytes(int64(len(TwitterJson)))
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, _ = json.Marshal(_GenericValue)
}
})
}
func BenchmarkEncoder_Parallel_Binding_StdLib(b *testing.B) {
_, _ = json.Marshal(&_BindingValue)
b.SetBytes(int64(len(TwitterJson)))
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, _ = json.Marshal(&_BindingValue)
}
})
}
具體的指標數據和統計結果,可參考benchmark_readme [14],總體結論如下:
常見優化思路有哪些?
定型編解碼
動態函數組裝
減少函數調用
code-gen
JIT
// 函數緩存
type cache struct {
functions map[*rtype]function
lock sync.Mutex
}
var (
global = func() [caches]*cache {
var caches [caches]*cache
for idx := range caches {
caches[idx] = &cache{functions: make(map[*rtype]function, 4)}
}
return caches
}()
)
func load(typ *rtype) (function, bool) {
do, ok := global[uintptr(unsafe.Pointer(typ))%caches].functions[typ]
return do, ok
}
func save(typ *rtype, do function) {
cache := global[uintptr(unsafe.Pointer(typ))%caches]
cache.lock.Lock()
cache.functions[typ] = do
cache.lock.Unlock()
}
泛型編解碼
-
數據反序列化的過程中,map 插入的開銷很高; -
在數據序列化過程中,map 遍歷也遠不如數組高效;
如果用一種與 json AST 更貼近的數據結構來描述,不但可以讓轉換過程更加簡單,甚至可以實現 lazy-load 。
復用編碼緩衝區
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)
newBuf := make([]byte, len(data))
copy(newBuf, buf)
buf.data = data
bufPool.Put(buf)
Sonic 庫為什麼性能好?
原理調研
它的優化思路可以分成離線和線上:
-
離線場景:針對 Go 語言編譯優化的不足,Sonic 核心計算函數使用 C 語言編寫,使用 Clang 的深度優化編譯選項,並開發了一套 asm2asm 工具,將完全優化的 x86 彙編翻譯成 plan9 彙編,載入到 Golang 運行時,以供調用。
-
線上場景:通過自定義 AST,實現了按需載入;採用 JIT 技術在運行時對模式對應的操作碼進行裝配,以 Golang 函數的形式緩存到堆外記憶體。這樣大大減少函數調用,同時也保證靈活性;
SIMD
asm2asm
JIT 彙編
RCU cache
自定義 AST
針對泛型編解碼,基於 map 開銷較大的考慮,Sonic 實現了更符合 json 結構的樹形 AST;通過自定義的一種通用的泛型數據容器 sonic-ast 替代 Go interface,從而提升性能。
用 node {type, length, pointer} 表示任意一個 json 數據節點,並結合樹與數組結構描述節點之間的層級關係。針對部分解析,考慮到解析和跳過之間的巨大速度差距,將 lazy-load 機制到 AST 解析器中,以一種更加自適應和高效的方式來減少多鍵查詢的開銷。
type Node struct {
v int64
t types.ValueType
p unsafe.Pointer
}
如何實現部分解析?
如何解決相同路徑查找重覆開銷的問題?
函數調用優化
-
無棧記憶體管理:自己維護變數棧(記憶體池),避免 Go 函數棧擴展。
-
自動生成跳轉表,加速 generic decoding 的分支跳轉。
-
使用寄存器傳參:儘量避免 memory load & store,將使用頻繁的變數放到固定的寄存器上,如:json buffer、結構體指針;
-
重寫函數調用:由於彙編函數不能內聯到 Go 函數中,函數調用引入的開銷甚至會抵消 SIMD 帶來的性能提升,因此在 JIT 中重新實現了一組輕量級的函數調用(維護全局函數表+函數 offset)。
業務實踐
適用場景
快速試用
import "github.com/brahma-adshonor/gohook"
func main() {
// 在main函數的入口hook當前使用的json庫(如encoding/json)
gohook.Hook(json.Marshal, sonic.Marshal, nil)
gohook.Hook(json.Unmarshal, sonic.Unmarshal, nil)
}
收益情況
使用事項
HTML Escape
func TestEncode(t *testing.T) {
data := map[string]string{"&&": "<>"}
// 標準庫
var w1 = bytes.NewBuffer(nil)
enc1 := json.NewEncoder(w1)
err := enc1.Encode(data)
assert.NoError(t, err)
// Sonic 庫
var w2 = bytes.NewBuffer(nil)
enc2 := encoder.NewStreamEncoder(w2)
err = enc2.Encode(data)
assert.NoError(t, err)
fmt.Printf("%v%v", w1.String(), w2.String())
}
// 運行結果:
{"\u0026\u0026":"\u003c\u003e"}
{"&&":"<>"}
若有需要可以通過下麵方式開啟:
import "github.com/bytedance/sonic/encoder"
v := map[string]string{"&&":"<>"}
ret, err := encoder.Encode(v, EscapeHTML) // ret == `{"\u0026\u0026":{"X":" \u003e"}}`
enc := encoder.NewStreamEncoder(w)
enc.SetEscapeHTML(true)
err := enc.Encode(obj)
大型模式問題
import (
"reflect"
"github.com/bytedance/sonic"
"github.com/bytedance/sonic/option"
)
func init() {
var v HugeStruct
// For most large types (nesting depth <= option.DefaultMaxInlineDepth)
err := sonic.Pretouch(reflect.TypeOf(v))
// with more CompileOption...
err := sonic.Pretouch(reflect.TypeOf(v),
// If the type is too deep nesting (nesting depth > option.DefaultMaxInlineDepth),
// you can set compile recursive loops in Pretouch for better stability in JIT.
option.WithCompileRecursiveDepth(loop),
// For a large nested struct, try to set a smaller depth to reduce compiling time.
option.WithCompileMaxInlineDepth(depth),
)
}
key 排序
import "github.com/bytedance/sonic"
import "github.com/bytedance/sonic/encoder"
// Binding map only
m := map[string]interface{}{}
v, err := encoder.Encode(m, encoder.SortMapKeys)
// Or ast.Node.SortKeys() before marshal
var root := sonic.Get(JSON)
err := root.SortKeys()
暫不支持 arm 架構
總結
-
不太推薦使用 Jsoniter 庫,原因在於: Go 1.8 之前,官方 Json 庫的性能就收到多方詬病。不過隨著 Go 版本的迭代,標準 json 庫的性能也越來越高,Jsonter 的性能優勢也越來越窄。如果希望有極致的性能,應該選擇 Easyjson 等方案而不是 Jsoniter,而且 Jsoniter 近年已經不活躍了。
-
比較推薦使用 Sonic 庫,因不論從性能和功能總體而言,Sonic 的表現的確很亮眼;此外,通過瞭解 Sonic 的內部實現原理,提供一種對於 cpu 密集型操作優化的“野路子”,即:通過編寫高性能的 C 代碼並經過優化編譯後供 Golang 直接調用。其實並不新鮮,因為實際上 Go 源碼中的一些 cpu 密集型操作底層就是編譯成了彙編後使用的,如:crypto 和 math。
參考資料:
-
深入 Go 中各個高性能 JSON 解析庫:https://www.luozhiyun.com/archives/535 -
Go 語言原生的 json 包有什麼問題?如何更好地處理 JSON 數據?:https://cloud.tencent.com/developer/article/1820473 -
go-json#how-it-works:https://github.com/goccy/go-json#how-it-works -
sonic :基於 JIT 技術的開源全場景高性能 JSON 庫 -
Introduction to Sonic:https://github.com/bytedance/sonic/blob/main/INTRODUCTION.md -
bytedance/sonic-readme:https://pkg.go.dev/github.com/bytedance/[email protected]#section-readme -
為位元組節省數十萬核的 json 庫 sonic:https://zhuanlan.zhihu.com/p/586050976
[1]https://pkg.go.dev/encoding/json?spm=ata.21736010.0.0.6e462b76wytEry
[2]https://pkg.go.dev/encoding/json?spm=ata.21736010.0.0.6e462b76wytEry
[3]https://github.com/valyala/fastjson?spm=ata.21736010.0.0.6e462b76wytEry
[4]https://github.com/tidwall/gjson?spm=ata.21736010.0.0.6e462b76wytEry
[5]https://github.com/buger/jsonparser?spm=ata.21736010.0.0.6e462b76wytEry
[6]https://github.com/json-iterator/go?spm=ata.21736010.0.0.6e462b76wytEry
[7]https://github.com/goccy/go-json?spm=ata.21736010.0.0.6e462b76wytEry
[8]https://github.com/mailru/easyjson?spm=ata.21736010.0.0.6e462b76wytEry
[9]https://github.com/bytedance/sonic?spm=ata.21736010.0.0.6e462b76wytEry
[10]https://github.com/zhao520a1a/go-base/blob/master/json/benchmark_test/bench.sh?spm=ata.21736010.0.0.6e462b76wytEry&file=bench.sh
[11]https://github.com/zhao520a1a/go-base/blob/master/json/testdata/small.go?spm=ata.21736010.0.0.6e462b76wytEry&file=small.go
[12]https://github.com/zhao520a1a/go-base/blob/master/json/testdata/medium.go?spm=ata.21736010.0.0.6e462b76wytEry&file=medium.go
[13]https://github.com/zhao520a1a/go-base/blob/master/json/testdata/large.json?spm=ata.21736010.0.0.6e462b76wytEry&file=large.json
[14]https://github.com/zhao520a1a/go-base/blob/master/json/benchmark_test/README.md?spm=ata.21736010.0.0.6e462b76wytEry&file=README.md
[15]https://github.com/simdjson/simdjson?spm=ata.21736010.0.0.6e462b76wytEry
[16]https://github.com/minio/simdjson-go?spm=ata.21736010.0.0.6e462b76wytEry
[17]https://github.com/brahma-adshonor/gohook?spm=ata.21736010.0.0.6e462b76wytEry
[19]https://github.com/bytedance/sonic?spm=ata.21736010.0.0.6e462b76wytEry#compatibility
[20]https://github.com/bytedance/sonic/issues/172?spm=ata.21736010.0.0.6e462b76wytEry
作者|趙金鑫(筆話)
本文來自博客園,作者:古道輕風,轉載請註明原文鏈接:https://www.cnblogs.com/88223100/p/Which-is-the-strongest-third-party-package-for-Go-JSON.html