专否 写文章

jerkzhang,stay hungry, stay foolish

Aug 13, 2024
Follow

为何不选择一种比JSON更高效的序列化规则?

今天在思考一件事情,为何不选择一种比JSON更高效的序列化规则?

在使用 golang 写自定义日志记录;最初的方案是,把要记录的内容,序列化成JSON格式的二进制数据存入数据库中。

首先,明确使用struct进行构造数据而不是map,因为对struct数据进行序列化,性能会更高。(别人对这事做的测试结论:对方测试指定的某个任务,使用 encoding/json 库序列化 struct 对象耗时约 0.018 秒,而使用 json-iterator 库耗时约 0.017 秒;对于 map,使用 encoding/json 库耗时约 0.270 秒,使用 json-iterator 库耗时约 0.223 秒)

因此,首选struct对象这是已经确定的事情。

然后这里就考虑了使用jsoniter代替标准库的"encoding/json",这样速度可以大大提升。

后来,我在权衡,是否此处一定要使用JSON格式进行序列化,性能的瓶颈可能就是JSON格式本身,如果自定义一个特殊的连接符,例如"-s_-^$",以及我自身能确定我要记录的数据中不会出现这个特殊字符串,那我就可以选择使用这个连接符,以及我对key名没有要求,通过约定数据的顺序,再以后需要使用的时候,基于顺序进行还原数据。其实就是利用Join进行序列化,利用Split进行反序列化,以约定的特殊连接符字符串进行连接,这样的性能,我认为会是高效的,而且省略了key的内容,占用空间大小甚至可能下降。

为了验证想法,写下面一个golang程序,作为验证以支持我的想法:

package main
import (
	"encoding/json"
	"fmt"
	"github.com/json-iterator/go"
	"strings"
	"time"
)
// 模拟数据
var testData = map[string]string{
	"a": "sfdsadddddddddddddddddddddddddadfdasfadfa",
	"c": "lsdfdafdadfldsalal91202",
}
// 测量并打印执行时间的函数
func measureDuration(fn func()) time.Duration {
	start := time.Now()
	fn()
	return time.Since(start)
}
// 使用 jsoniter 序列化
func serializeWithJsoniter() ([]byte, error) {
	return jsoniter.Marshal(testData)
}
// 使用标准库序列化
func serializeWithStdlib() ([]byte, error) {
	return json.Marshal(testData)
}
// 手动构造字符串
func manualConcatenation() []byte {
	separator := "-s_-^$"
	return []byte(strings.Join([]string{testData["a"], separator, testData["c"]}, ""))
}
func main() {
	// 测量 jsoniter 序列化性能
	durationJsoniter := measureDuration(func() {
		for i := 0; i < 1000; i++ {
			_, _ = serializeWithJsoniter()
		}
	})
	fmt.Printf("jsoniter serialization took: %v\n", durationJsoniter)
	// 测量标准库序列化性能
	durationStdlib := measureDuration(func() {
		for i := 0; i < 1000; i++ {
			_, _ = serializeWithStdlib()
		}
	})
	fmt.Printf("stdlib serialization took: %v\n", durationStdlib)
	// 测量手动构造字符串性能
	durationManual := measureDuration(func() {
		for i := 0; i < 1000; i++ {
			_ = manualConcatenation()
		}
	})
	fmt.Printf("manual concatenation took: %v\n", durationManual)
}

上述的测试结果是:

jsoniter serialization took: 265.875µs
stdlib serialization took: 441.041µs
manual concatenation took: 68.25µs

"manual concatenation took"就是自定义基于连接符的序列化方案,明显是优越与JSON格式的序列化。不过上面的方法没有使用struct对象,改成struct对象,如下:

package main
import (
	"encoding/json"
	"fmt"
	"github.com/json-iterator/go"
	"strings"
	"time"
)
// 定义一个用于测试的 struct
type TestStruct struct {
	A string `json:"a"`
	C string `json:"c"`
}
// 测试数据
var testStructInstance = TestStruct{
	A: "sfdsadddddddddddddddddddddddddadfdasfadfa",
	C: "lsdfdafdadfldsalal91202",
}
// 测量并打印执行时间的函数
func measureDuration(fn func(), description string) {
	start := time.Now()
	fn()
	duration := time.Since(start)
	fmt.Printf("%s took: %v\n", description, duration)
}
// 使用 jsoniter 序列化 TestStruct
func serializeWithJsoniter() ([]byte, error) {
	return jsoniter.Marshal(testStructInstance)
}
// 使用标准库序列化 TestStruct
func serializeWithStdlib() ([]byte, error) {
	return json.Marshal(testStructInstance)
}
// 手动构造字符串
func manualConcatenation() []byte {
	separator := "-s_-^$"
	return []byte(strings.Join([]string{testStructInstance.A, separator, testStructInstance.C}, ""))
}
func main() {
	// 测量 jsoniter 序列化性能
	measureDuration(func() {
		for i := 0; i < 1000; i++ {
			_, _ = serializeWithJsoniter()
		}
	}, "jsoniter serialization")
	// 测量标准库序列化性能
	measureDuration(func() {
		for i := 0; i < 1000; i++ {
			_, _ = serializeWithStdlib()
		}
	}, "stdlib serialization")
	// 测量手动构造字符串性能
	measureDuration(func() {
		for i := 0; i < 1000; i++ {
			_ = manualConcatenation()
		}
	}, "manual concatenation")
}

测试结果如下:

jsoniter serialization took: 233.042µs
stdlib serialization took: 258.791µs
manual concatenation took: 59.083µs

依然可见基于特殊连接符字符串去进行序列化的思路是可行的,而且性能提高4-8倍 。(其实是因为上述只是简单示例,随着复杂度提高,性能提高数十倍甚至百倍都是可能的)



之后我有进一步修改了测试数据,让测试数据更复杂一些,测试代码如下:

package main
import (
	"encoding/json"
	"fmt"
	"github.com/json-iterator/go"
	"strings"
	"time"
)
// 定义一个具有15个字段的复杂 struct
type TestStruct struct {
	Field1  string `json:"field1"`
	Field2  string `json:"field2"`
	Field3  string `json:"field3"`
	Field4  string `json:"field4"`
	Field5  string `json:"field5"`
	Field6  string `json:"field6"`
	Field7  string `json:"field7"`
	Field8  string `json:"field8"`
	Field9  string `json:"field9"`
	Field10 string `json:"field10"`
	Field11 string `json:"field11"`
	Field12 string `json:"field12"`
	Field13 string `json:"field13"`
	Field14 string `json:"field14"`
	Field15 string `json:"field15"`
}
// 测试数据,包含15个字段的复杂结构体实例
var testStructInstance = TestStruct{
	Field1:  "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
	Field2:  "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
	Field3:  "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.",
	Field4:  "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum.",
	Field5:  "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia.",
	Field6:  "Deserunt mollit anim id est laborum.",
	Field7:  "Maecenas sed diam eget risus varius blandit sit amet non magna.",
	Field8:  "Curabitur blandit tempus porttitor.",
	Field9:  "Vestibulum id ligula porta felis euismod semper.",
	Field10: "Integer posuere erat a ante venenatis dapibus posuere velit aliquet.",
	Field11: "Donec id elit non mi porta gravida at eget metus.",
	Field12: "Maecenas faucibus mollis interdum.",
	Field13: "Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh.",
	Field14: "Pellentesque sed dolor. Aliquam congue fermentum nisl, non tincidunt.",
	Field15: "Fusce pellentesque suscipit sagittis.",
}
// 测量并打印执行时间的函数
func measureDuration(fn func(), description string) {
	start := time.Now()
	fn()
	duration := time.Since(start)
	fmt.Printf("%s took: %v\n", description, duration)
}
// 使用 jsoniter 序列化 TestStruct
func serializeWithJsoniter() ([]byte, error) {
	return jsoniter.Marshal(testStructInstance)
}
// 使用标准库序列化 TestStruct
func serializeWithStdlib() ([]byte, error) {
	return json.Marshal(testStructInstance)
}
// 手动构造字符串
func manualConcatenation() []byte {
	// 手动连接所有字段,使用特定的分隔符
	separator := "-s_-^$"
	fields := []string{
		testStructInstance.Field1,  // ... 到 Field15
		// ... 省略中间字段以节省空间
		testStructInstance.Field15,
	}
	return []byte(strings.Join(fields, separator))
}
func main() {
	// 测量 jsoniter 序列化性能
	measureDuration(func() {
		for i := 0; i < 1000; i++ {
			_, _ = serializeWithJsoniter()
		}
	}, "jsoniter serialization")
	// 测量标准库序列化性能
	measureDuration(func() {
		for i := 0; i < 1000; i++ {
			_, _ = serializeWithStdlib()
		}
	}, "stdlib serialization")
	// 测量手动构造字符串性能
	measureDuration(func() {
		for i := 0; i < 1000; i++ {
			_ = manualConcatenation()
		}
	}, "manual concatenation")
}

测试结果如下:

jsoniter serialization took: 1.396708ms
stdlib serialization took: 1.359458ms
manual concatenation took: 62.875µs

综上可见,json序列化的性能会随着复杂度的提升而大幅受限;但是基于连接符的序列化方案基本上性能变化不大,其性能承载力很高。所以,在一些场景中,基于连接符的序列化方案是原优于json这类序列化方案。

喜欢这个文章 | 分享 | 新建跟帖