10x faster than reflect.DeepEqual
構建 CovenantSQL 的時候遇到一個棘手的問題:
假設我們現在有一個比較複雜的結構體 ComplexStruct
type SimpleStruct struct { Str string } type ComplexStruct struct { Simple map[string]*SimpleStruct Other []byte Nums[8]float64 }
我們需要在 10000 個 ComplexStruct 型別的變數 Slice 裡找到和已知變數內容相同的。
首先,最笨的辦法當然是手寫:
var target = ComplexStruct { ... } for _, cs := range csSlice { for k, v := range cs.Simple { if v1, ok := target.Simple[k]; ok { // 算了老子不幹了 } else { } } }
DeepEqual
聰明的朋友都會用reflect.DeepEqual
來比較:
reflect.DeepEqual(v1, v2)
但我們知道,無論是哪種語言,基於反射(reflect)的各種方法都會比普通的函式呼叫至少慢上 1 ~ 2 個數量級。
有沒有又懶又快的方法呢?我反正是沒找到,所以自己基於一個 MsgPack 的庫 寫了一個HashStatePack ,主要功能如下:
- 根據已經定義好的 golang 結構體自動生成程式碼,避免搬磚
-
比
reflect.DeepEqual
快大概10 ~ 20 倍,測試在此
BenchmarkCompare/benchmark_reflect-8 20074 ns/op //reflect.DeepEqual
BenchmarkCompare/benchmark_hsp-8 2322 ns/op
BenchmarkCompare/benchmark_hsp_1_cached-8 1101 ns/op
BenchmarkCompare/benchmark_hsp_both_cached-8 11.2 ns/op
為什麼不用……
為什麼不用 Protobuf 或者 MsgPack,甚至 JSON 序列化之後再比較?
- JSON: 記憶體使用效率太低,特別是遇到 Binary 型別。
- 大部分 JSON、MsgPack 的庫也是基於反射的實現,也很慢。
- Prorobuf: struct must defined in proto language, and other limitations discussedhere
- 最後,也是最棘手的問題:Golang 的設計者為了避免大家錯誤的依賴 map 的順序,在迭代 map 的時候故意加入了一定的洗牌演算法。這就導致幾乎針對同樣一個 map 的 range 每次的結果都不一樣。
原理
hsp:"xxx"
例如,我們開頭例子中的 ComplexStruct,生成的核心程式碼如下:
// MarshalHash marshals for hash func (z *ComplexStruct) MarshalHash() (o []byte, err error) { var b []byte o = hsp.Require(b, z.Msgsize()) // map header, size 3 o = append(o, 0x83) o = hsp.AppendArrayHeader(o, uint32(8)) for za0003 := range z.Nums { o = hsp.AppendFloat64(o, z.Nums[za0003]) } o = hsp.AppendBytes(o, z.Other) o = hsp.AppendMapHeader(o, uint32(len(z.Simple))) za0001Slice := make([]string, 0, len(z.Simple)) for i := range z.Simple { za0001Slice = append(za0001Slice, i) } sort.Strings(za0001Slice) for _, za0001 := range za0001Slice { za0002 := z.Simple[za0001] o = hsp.AppendString(o, za0001) if za0002 == nil { o = hsp.AppendNil(o) } else { // map header, size 1 o = append(o, 0x81) o = hsp.AppendString(o, za0002.Str) } } return }
剩下的程式碼就可以這麼寫了:
bts1, _ := v1.MarshalHash() bts2, _ := v2.MarshalHash() if bytes.Equal(bts1, bts2) { ... }
針對我們遇到的在大量 Slice 中 尋找相同內容的問題,如果我們對生成的[]byte
,進行一次雜湊。然後用雜湊只作為 key,物件作為 Value,效率將會非常的高。
HashStablePack 目前主要被CovenantSQL 用來做簽名、校驗,以及區塊雜湊計算上,希望可以幫到你:-)
怎麼使用 HSP
go get -u github.com/CovenantSQL/HashStablePack/hsp
在你需要生成的原始檔頭部加上
//go:generate hsp
執行
go generate ./...
程式碼、測試程式碼,就統統生成好了