golang json 效能分析

語言: CN / TW / HK

在看程式碼的過程中,發現很多程式碼並沒有使用golang自帶的json,而是使用json-iterator去做的json的編解碼,好奇之下,便去研究了一下。

json

Json 作為一種重要的資料格式,具有良好的可讀性以及自描述性,廣泛地應用在各種資料傳輸場景中。Go 語言裡面原生支援了這種資料格式的序列化以及反序列化,內部使用反射機制實現,效能有點差,在高度依賴 json 解析的應用裡,往往會成為效能瓶頸。

那我們今天的主角是GO的json,主要分析2個包,就是原生的encode/json和json-iterator。

測試

作為效能而言,我自然要先做壓測,go自帶了benchmark可以很方便我們對程式碼進行資料測試。

簡單的結構沒有測試意義,因此取了一個介面的返回值當測試資料。

package test

import (
	"encoding/json"
	json1 "github.com/json-iterator/go"
	"testing"
)


var str = `{
    "stat": 1,
    "code": 0,
    "msg": "成功",
    "data": {
        "courseInfos": [
            {
                "courseId": "161935",
                "course_name": "高三5科語數英物化提分特訓班(20課時)",
                "gradeId": "13",
                "gradeName": "高三",
                "subjectName": "數學",
                "difficultyName": "目標A+",
                "type1Name": "特訓班",
                "termIds": "4",
                "schoolTime": "1月29日-1月31日上課(詳情見大綱)",
                "price": "799",
                "actualPrice": 20,
                "subjectId": 2,
                "type_1_id": "2064",
                "type_2_id": "2656",
                "type_3_id": "2661"
            }
        ],
        "ext": {
            "price": "799",
            "sale": "20",
            "wxShareObj": "{\"title\":\"語數雙科提分特訓班\",\"desc\":\"20元搶20課時名師直播好課,下單加送國風限量教輔禮包!\",\"imgUrl\":\"https://activity.xueersi.com/topic/growth/common/images/common/xes-logo.png\",\"miniImgUrl\":\"https://hw.xesimg.com/biz-growth-storage/operations/groupon/20201215/0f4f57c8c6f88b66b059881cfb050527.png\"}",
            "abTestPackage": "{\"h5\":[\"20_20ChineseA_sucaiH5\",\"Azhifudanye_H5\",\"Apintuan_H5\",\"Axueyuanpinglun_ceshi\"],\"smallProgram\":[]}"
        },
        "resourceConfig": {
            "bookImg": [
                {
                    "type": "img",
                    "url": "https://ek.xesimg.com/biz-growth-storage/activity/upload/20201216/86724030cc04b4ea029fe31e4042880c.png",
                    "name": "初高H5&小程式隨材圖 .png"
                }
            ],
            "detailImg": [
                {
                    "type": "img",
                    "url": "https://oo.xesimg.com/biz-growth-storage/activity/upload/20210126/7e9bd0cb1810c9de1432bc8c570ce3f0.png",
                    "name": "[email protected]"
                },
                {
                    "type": "img",
                    "url": "https://ek.xesimg.com/biz-growth-storage/activity/upload/20210126/17bdceacf83feda19ee854b50c469152.png",
                    "name": "[email protected]"
                },
                {
                    "type": "img",
                    "url": "https://hw.xesimg.com/biz-growth-storage/activity/upload/20210126/21189d1f915feeaf0f103a5dfbe77950.png",
                    "name": "[email protected]"
                },
                {
                    "type": "img",
                    "url": "https://mr.xesimg.com/biz-growth-storage/activity/upload/20210126/fb106f25b740fc7e559bab5568958fe5.png",
                    "name": "[email protected]"
                },
                {
                    "type": "img",
                    "url": "https://oo.xesimg.com/biz-growth-storage/activity/upload/20210126/93f595fa4cd4fd72683f2faf1b33e86b.png",
                    "name": "[email protected]"
                },
                {
                    "type": "img",
                    "url": "https://oo.xesimg.com/biz-growth-storage/activity/upload/20210126/b5d789b9aa8833367d4d74af5d20bfc5.png",
                    "name": "[email protected]"
                },
                {
                    "type": "img",
                    "url": "https://oo.xesimg.com/biz-growth-storage/activity/upload/20210126/389308e953ac695a972840c796443de5.png",
                    "name": "[email protected]"
                },
                {
                    "type": "img",
                    "url": "https://oo.xesimg.com/biz-growth-storage/activity/upload/20210126/29465ee6a664f36c4ae4b154f60c5b01.png",
                    "name": "矩陣@2x.png"
                }
            ],
            "headImg": [
                {
                    "type": "img",
                    "url": "https://ek.xesimg.com/biz-growth-storage/activity/upload/20210126/45e73133144cb179a1f33d85e5e02c68.png",
                    "name": "頭圖@2x.png"
                }
            ],
            "bookTextDesc": "多科目組合課程,為保障學習效果,暫不支援調課哦",
            "bookTextWx": "https://ek.xesimg.com/biz-growth-storage/operations/groupon/20201216/cda7dc272b4757efa7a21c3de30f87e8.png",
            "grade_id": "13",
            "feitoufang": "140012,140013,140014",
            "videoInfo": "{\"videoUrl\":\"https://activity.xueersi.com/oss/resource/%E9%AB%98%E4%B8%AD-1611580251682.mp4\",\"videoPoster\":\"https://activity.xueersi.com/oss/resource/%E9%AB%98%E4%B8%AD-1611658207877.png\"}"
        }
    }
}`

type T struct {
	Stat int    `json:"stat"`
	Code int    `json:"code"`
	Msg  string `json:"msg"`
	Data struct {
		CourseInfos []struct {
			CourseId       string `json:"courseId"`
			CourseName     string `json:"course_name"`
			GradeId        string `json:"gradeId"`
			GradeName      string `json:"gradeName"`
			SubjectName    string `json:"subjectName"`
			DifficultyName string `json:"difficultyName"`
			Type1Name      string `json:"type1Name"`
			TermIds        string `json:"termIds"`
			SchoolTime     string `json:"schoolTime"`
			Price          string `json:"price"`
			ActualPrice    int    `json:"actualPrice"`
			SubjectId      int    `json:"subjectId"`
			Type1Id        string `json:"type_1_id"`
			Type2Id        string `json:"type_2_id"`
			Type3Id        string `json:"type_3_id"`
		} `json:"courseInfos"`
		Ext struct {
			Price         string `json:"price"`
			Sale          string `json:"sale"`
			WxShareObj    string `json:"wxShareObj"`
			AbTestPackage string `json:"abTestPackage"`
		} `json:"ext"`
		ResourceConfig struct {
			BookImg []struct {
				Type string `json:"type"`
				Url  string `json:"url"`
				Name string `json:"name"`
			} `json:"bookImg"`
			DetailImg []struct {
				Type string `json:"type"`
				Url  string `json:"url"`
				Name string `json:"name"`
			} `json:"detailImg"`
			HeadImg []struct {
				Type string `json:"type"`
				Url  string `json:"url"`
				Name string `json:"name"`
			} `json:"headImg"`
			BookTextDesc string `json:"bookTextDesc"`
			BookTextWx   string `json:"bookTextWx"`
			GradeId      string `json:"grade_id"`
			Feitoufang   string `json:"feitoufang"`
			VideoInfo    string `json:"videoInfo"`
		} `json:"resourceConfig"`
	} `json:"data"`
}
//因為原生也不進行任何引數設定,所以都使用預設配置
var Json1 = json1.ConfigDefault
var T1 T
var Tmap map[string]interface{}
var TByte []byte
func init() {
	TByte = []byte(str)
	json1.Unmarshal(TByte, &T1)
	json1.Unmarshal(TByte, &Tmap)
}

func BenchmarkEncodeStructJson1(b *testing.B) {
	for i := 0; i < b.N; i++ {
		Json1.Marshal(T1)
	}
}

func BenchmarkEncodeStructJson(b *testing.B) {
	for i := 0; i < b.N; i++ { 
		json.Marshal(T1)
	}
}

func BenchmarkEncodeMapJson1(b *testing.B) {
	for i := 0; i < b.N; i++ {
		Json1.Marshal(Tmap)
	}
}

func BenchmarkEncodeMapJson(b *testing.B) {
	for i := 0; i < b.N; i++ {
		json.Marshal(Tmap)
	}
}

func BenchmarkDecodeStructJson1(b *testing.B) {
	var TStruct T
	for i := 0; i < b.N; i++ {
		Json1.Unmarshal(TByte, &TStruct)
	}
}

func BenchmarkDecodeStructJson(b *testing.B) {
	var TStruct T
	for i := 0; i < b.N; i++ {
		json.Unmarshal(TByte, &TStruct)
	}
}

func BenchmarkDecodeMapJson1(b *testing.B) {
	var TTMap map[string]interface{}
	for i := 0; i < b.N; i++ {
		Json1.Unmarshal(TByte, &TTMap)
	}
}

func BenchmarkDecodeMapJson(b *testing.B) {
	var TTMap map[string]interface{}
	for i := 0; i < b.N; i++ {
		json.Unmarshal(TByte, &TTMap)
	}
}

程式碼準備好之後,就直接開始測試吧

go test -bench=. -benchtime=3s -benchmem -run=none

測試結果

goos: darwin
goarch: amd64
pkg: test
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkEncodeStructJson1-12             450108              6911 ns/op            3365 B/op          2 allocs/op
BenchmarkEncodeStructJson-12              538249              6508 ns/op            3365 B/op          2 allocs/op
BenchmarkEncodeMapJson1-12                281665             11958 ns/op            4634 B/op         31 allocs/op
BenchmarkEncodeMapJson-12                 123217             28541 ns/op           12701 B/op        219 allocs/op
BenchmarkDecodeStructJson1-12             307185             10635 ns/op            4521 B/op        100 allocs/op
BenchmarkDecodeStructJson-12              103711             34813 ns/op            3512 B/op         64 allocs/op
BenchmarkDecodeMapJson1-12                149858             23326 ns/op           13663 B/op        310 allocs/op
BenchmarkDecodeMapJson-12                  96092             37011 ns/op           11801 B/op        231 allocs/op

測試分析

  1. encode結構體的情況下,資料差異不大,原生甚至一定程度上稍微領先了。
  2. encode Map的情況下,原生效能下降,看到大量的記憶體分配。
  3. decode結構體的情況下,原生效能不如json-iterator,但是記憶體擦操作次數少於json-iterator。
  4. decode Map的情況下,同上。

綜上,我做一些推斷:

encode原始碼分析比較

原生marshal,基本使用的就是遞迴反射

func (e *encodeState) marshal(v interface{}, opts encOpts) (err error) {
	defer func() {
		if r := recover(); r != nil {
			if je, ok := r.(jsonError); ok {
				err = je.error
			} else {
				panic(r)
			}
		}
	}()
	e.reflectValue(reflect.ValueOf(v), opts)
	return nil
}

json-iterator也思路一樣

// WriteVal copy the go interface into underlying JSON, same as json.Marshal
func (stream *Stream) WriteVal(val interface{}) {
	if nil == val {
		stream.WriteNil()
		return
	}
	cacheKey := reflect2.RTypeOf(val)
	encoder := stream.cfg.getEncoderFromCache(cacheKey)
	if encoder == nil {
		typ := reflect2.TypeOf(val)
		encoder = stream.cfg.EncoderOf(typ)
	}
	encoder.Encode(reflect2.PtrOf(val), stream)
}

但是仔細觀察:

  1. 他的程式碼對反射包進行了重新設計
  2. 對不同的型別構建了不同的encoder

通過閱讀程式碼,主要是通過每個重複型別,進行了判定此型別的encoder是否存在,存在則取之前的encoder,因此大量減少的encoder的建立和銷燬操作。

這一塊有個細節的資料結構操作,通過型別type地址去做。我摘取了部分原始碼出來演示並測試。

func TestUnsafePointer(t *testing.T) {
	s := unpackEFace(&T1)
	fmt.Println(uintptr(s.rtype))
	var T2 T
	s2 := unpackEFace(&T2)
	fmt.Println(uintptr(s2.rtype))
}

type eface struct {
	rtype unsafe.Pointer
	data  unsafe.Pointer
}

func unpackEFace(obj interface{}) *eface {
	return (*eface)(unsafe.Pointer(&obj))
}
=== RUN   TestUnsafePointer
18478592
18478592
--- PASS: TestUnsafePointer (0.00s)

因此這塊就能解釋為什麼json-iterator的在encode的時候記憶體操作次數會遠遠低於原生。

但是這塊有個疑問,為什麼struct encode原生並不差,只是map差?

查閱原始碼,發現一個這玩意: // ConfigCompatibleWithStandardLibrary tries to be 100% compatible with standard library behavior var ConfigCompatibleWithStandardLibrary = Config{ EscapeHTML: true, SortMapKeys: true, ValidateJsonRawMessage: true, }.Froze()

原來預設的並不是和官方結果百分百相似,因此,將測試用例調整為此物件。

json-iterator預設配置:
BenchmarkEncodeMapJson1-12                 96573             12353 ns/op            4633 B/op         31 allocs/op
BenchmarkEncodeMapJson-12                  41634             28370 ns/op           12701 B/op        219 allocs/op

json-iterator標準配置:
BenchmarkEncodeMapJson1-12                141724             25356 ns/op           11113 B/op        158 allocs/op
BenchmarkEncodeMapJson-12                 124508             29054 ns/op           12701 B/op        219 allocs/op

可以看出,記憶體雖然有一定優化,但是不如之前明顯了。

  1. struct 多次壓縮時,encoding 中會快取 name 資訊, 以及對應val的型別,直接呼叫相應的encoder 即可;相反,map 則每次需要對key 做反射,根據型別判斷獲取key的值,val值也需要反射獲取相應的encoder,時間浪費較多。
  2. map 在做json 的解析的結果,會做排序操作。若修改原始碼,將排序操作遮蔽,key 越多,需要的時間越多。

而經過測試,結果體的結果基本不變,原生稍微比json-iterator優勢。

decode原始碼分析比較

其實兩者的思路都是一樣,遍歷字串,當遇到特殊的字串的時候進行下一步操作。

原生的使用的是一次遍歷完,並且通過反射填充資料,主要分為3種方式: object、array、其他。

原生json有幾個重要物件,一個scanner物件,一個decodeState物件。

  1. scanner :A scanner is a JSON scanning state machine. 官方的說法就是一個狀態記錄機。用其中一個parseState記錄了特殊操作符的位置,可以理解為stack結構。
  2. decodeState表示解碼JSON值時的狀態,可以理解為步操作機。
type scanner struct {
	step func(*scanner, byte) int
	endTop bool
	parseState []int
	err error
	bytes int64
}
type decodeState struct {
	data         []byte
	off          int // next read offset in data
	opcode       int // last read result
	scan         scanner
	errorContext struct { // provides context for type errors
		Struct     reflect.Type
		FieldStack []string
	}
	savedError            error
	useNumber             bool
	disallowUnknownFields bool
}

而json-iterator只有一個物件Iterator,作用就是用於迭代當前的bytes資料。

type Iterator struct {
	cfg              *frozenConfig
	reader           io.Reader
	buf              []byte
	head             int
	tail             int
	depth            int
	captureStartedAt int
	captured         []byte
	Error            error
	Attachment       interface{} // open for customized decoder
}
type ValDecoder interface {
	Decode(ptr unsafe.Pointer, iter *Iterator)
}

另外json-iterator提供了一個可供適配的介面ValDecoder用於解析不同的資料型別。也就是說,每個不同的資料型別,會生成新的物件,這也就能解釋,為啥在decode json的時候,json-iterator的記憶體分配操作次數會大於原生。

為了證實我的推論,我測試一個最簡單的json。

var TBytes1 = []byte(`{
	"foo":"test"
}`)
func BenchmarkSampleDecodeMapJson1(b *testing.B) {
	var TTMap map[string]interface{}
	for i := 0; i < b.N; i++ { //use b.N for looping
		Json1.Unmarshal(TBytes1, &TTMap)
	}
}

func BenchmarkSampleDecodeMapJson(b *testing.B) {
	var TTMap map[string]interface{}
	for i := 0; i < b.N; i++ { //use b.N for looping
		json.Unmarshal(TBytes1, &TTMap)
	}
}
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkSampleDecodeMapJson1-12         3971719               273.0 ns/op            56 B/op          5 allocs/op
BenchmarkSampleDecodeMapJson-12          1708197               687.0 ns/op           264 B/op          8 allocs/op

其他

ummashal測試過程中,發現錯誤的json格式的話,官方會極快的返回並很少的記憶體操作,但是json-iterator會去遍歷生成迭代期物件直到錯誤為止。因此在使用過程中,如果決定使用json-iterator,最好保證是正確的資料。

下面是錯誤資料的decode的操作結果。

BenchmarkSampleDecodeMapJson1-12          423016              2469 ns/op            1673 B/op         41 allocs/op
BenchmarkSampleDecodeMapJson-12          1821427               656.6 ns/op           256 B/op          5 allocs/op

而官方做的是load資料的時候做檢測,生成特殊op的操作位。

// checkValid verifies that data is valid JSON-encoded data.
// scan is passed in for use by checkValid to avoid an allocation.
func checkValid(data []byte, scan *scanner) error {
	scan.reset()
	for _, c := range data {
		scan.bytes++
		if scan.step(scan, c) == scanError {
			return scan.err
		}
	}
	if scan.eof() == scanError {
		return scan.err
	}
	return nil
}

而json-iterator並沒有這個檢測機制,好處是不用遍歷2次。但是同樣,如果是錯誤的json,會像上面所述的一樣,錯誤直到解析不對為止。

更多的優化細節:

官方優化思路

另外,我當前測試的版本是1.16..5,而歷史版本也需要測試,因此我選取版本為1.12.、1.14.,因此,最後追加這兩個版本的測試結果。

[email protected]% /usr/local/go112/bin/go test -bench=. -benchtime=1s -benchmem -run=none 
goos: darwin
goarch: amd64
pkg: test
BenchmarkEncodeStructJson1-12    	  200000	      7487 ns/op	    3366 B/op	       2 allocs/op
BenchmarkEncodeStructJson-12     	  200000	      7353 ns/op	    3368 B/op	       2 allocs/op
BenchmarkEncodeMapJson1-12       	   50000	     26436 ns/op	   11744 B/op	     173 allocs/op
BenchmarkEncodeMapJson-12        	   50000	     30261 ns/op	   12821 B/op	     219 allocs/op
BenchmarkDecodeStructJson1-12    	  100000	     12610 ns/op	    4529 B/op	     100 allocs/op
BenchmarkDecodeStructJson-12     	   30000	     46576 ns/op	    3392 B/op	      61 allocs/op
BenchmarkDecodeMapJson1-12       	   50000	     24597 ns/op	   13704 B/op	     310 allocs/op
BenchmarkDecodeMapJson-12        	   30000	     48988 ns/op	   11908 B/op	     234 allocs/op
PASS
ok  	test	13.619s
[email protected]% /usr/local/go114/bin/go test -bench=. -benchtime=1s -benchmem -run=none 
goos: darwin
goarch: amd64
pkg: test
BenchmarkEncodeStructJson1-12    	  161550	      7294 ns/op	    3364 B/op	       2 allocs/op
BenchmarkEncodeStructJson-12     	  165632	      7202 ns/op	    3365 B/op	       2 allocs/op
BenchmarkEncodeMapJson1-12       	   44802	     26748 ns/op	   11716 B/op	     173 allocs/op
BenchmarkEncodeMapJson-12        	   38680	     30433 ns/op	   12821 B/op	     219 allocs/op
BenchmarkDecodeStructJson1-12    	   93560	     12380 ns/op	    4529 B/op	     100 allocs/op
BenchmarkDecodeStructJson-12     	   30903	     39145 ns/op	    3520 B/op	      64 allocs/op
BenchmarkDecodeMapJson1-12       	   47817	     25344 ns/op	   13703 B/op	     310 allocs/op
BenchmarkDecodeMapJson-12        	   29022	     41598 ns/op	   11921 B/op	     234 allocs/op
PASS
ok  	test	11.808s

可以看出,整體結果和1.16幾乎沒有差距。