詳解如何讓Go語言中的反射加快
最近讀到一篇關(guān)于 Go 反射的文章,作者通過反射給結(jié)構(gòu)體填充字段值的案例,充分利用 Go 的各種內(nèi)在機(jī)理,逐步探討讓代碼運(yùn)行得更快的姿勢。
文章(原文地址:https://philpearl.github.io/post/aintnecessarilyslow/)非常有學(xué)習(xí)價(jià)值,故翻譯整理了下來。
不要使用反射,除非你真的需要。但是當(dāng)你不使用反射時(shí),不要認(rèn)為這是因?yàn)榉瓷浜苈?,它也可以很快?/p>
反射允許你在運(yùn)行時(shí)獲得有關(guān) Go 類型的信息。如果你曾經(jīng)愚蠢地嘗試編寫 json.Unmarshal 之類的新版本,本文將探討的就是如何使用反射來填充結(jié)構(gòu)體值。
切入點(diǎn)案例
我們以一個(gè)簡單的案例為切入點(diǎn),定義一個(gè)結(jié)構(gòu)體 SimpleStruct,它包括兩個(gè) int 類型字段 A 和 B。
type SimpleStruct struct { A int B int }
假如我們接收到了 JSON 數(shù)據(jù) {"B": 42},想要對其進(jìn)行解析并且將字段 B 設(shè)置為 42。
在下文,我們將編寫一些函數(shù)來實(shí)現(xiàn)這一點(diǎn),它們都會將 B 設(shè)置為 42。
如果我們的代碼只適用于 SimpleStruct,這完全是不值一提的。
func populateStruct(in *SimpleStruct) { in.B = 42 }
反射基本版
但是,如果我們是要做一個(gè) JSON 解析器,這意味著我們并不能提前知道結(jié)構(gòu)類型。我們的解析器代碼需要接收任何類型的數(shù)據(jù)。
在 Go 中,這通常意味著需要采用 interface{} (空接口)參數(shù)。然后我們可以使用 reflect 包檢查通過空接口參數(shù)傳入的值,檢查它是否是指向結(jié)構(gòu)體的指針,找到字段 B 并用我們的值填充它。
代碼將如下所示。
func populateStructReflect(in interface{}) error { val := reflect.ValueOf(in) if val.Type().Kind() != reflect.Ptr { return fmt.Errorf("you must pass in a pointer") } elmv := val.Elem() if elmv.Type().Kind() != reflect.Struct { return fmt.Errorf("you must pass in a pointer to a struct") } fval := elmv.FieldByName("B") fval.SetInt(42) return nil }
讓我們通過基準(zhǔn)測試看看它有多快。
func BenchmarkPopulateReflect(b *testing.B) { b.ReportAllocs() var m SimpleStruct for i := 0; i < b.N; i++ { if err := populateStructReflect(&m); err != nil { b.Fatal(err) } if m.B != 42 { b.Fatalf("unexpected value %d for B", m.B) } } }
結(jié)果如下。
BenchmarkPopulateReflect-16 15941916 68.3 ns/op 8 B/op 1 allocs/op
這是好還是壞?好吧,內(nèi)存分配可從來不是好事。你可能想知道為什么需要在堆上分配內(nèi)存來將結(jié)構(gòu)體字段設(shè)置為 42(可以看這個(gè) issue:https://github.com/golang/go/issues/2320)。但總體而言,68ns 的時(shí)間并不長。在通過網(wǎng)絡(luò)發(fā)出任何類型的請求時(shí)間中,你可以容納很多 68ns。
優(yōu)化一:加入緩存策略
我們能做得更好嗎?好吧,通常我們運(yùn)行的程序不會只做一件事然后停止。他們通常一遍又一遍地做著非常相似的事情。因此,我們可以設(shè)置一些東西以使重復(fù)的事情速度變快嗎?
如果仔細(xì)查看我們正在執(zhí)行的反射檢查,我們會發(fā)現(xiàn)它們都取決于傳入值的類型。如果我們將類型結(jié)果緩存起來,那么對于每種類型而言,我們只會進(jìn)行一次檢查。
我們再來考慮內(nèi)存分配的問題。之前我們調(diào)用 Value.FieldByName 方法,實(shí)際是 Value.FieldByName 調(diào)用 Type.FieldByName,其調(diào)用 structType.FieldByName,最后調(diào)用 structType.Field 來引起內(nèi)存分配的。我們可以在類型上調(diào)用 FieldByName 并緩存一些東西來獲取 B 字段的值嗎?實(shí)際上,如果我們緩存 Field.Index,就可以使用它來獲取字段值而無需分配。
新代碼版本如下
var cache = make(map[reflect.Type][]int) func populateStructReflectCache(in interface{}) error { typ := reflect.TypeOf(in) index, ok := cache[typ] if !ok { if typ.Kind() != reflect.Ptr { return fmt.Errorf("you must pass in a pointer") } if typ.Elem().Kind() != reflect.Struct { return fmt.Errorf("you must pass in a pointer to a struct") } f, ok := typ.Elem().FieldByName("B") if !ok { return fmt.Errorf("struct does not have field B") } index = f.Index cache[typ] = index } val := reflect.ValueOf(in) elmv := val.Elem() fval := elmv.FieldByIndex(index) fval.SetInt(42) return nil }
因?yàn)闆]有任何內(nèi)存分配,新的基準(zhǔn)測試變得更快。
BenchmarkPopulateReflectCache-16 35881779 30.9 ns/op 0 B/op 0 allocs/op
優(yōu)化二:利用字段偏移量
我們能做得更好嗎?好吧,如果我們知道結(jié)構(gòu)體字段 B 的偏移量并且知道它是 int 類型,就可以將其直接寫入內(nèi)存。我們可以從接口中恢復(fù)指向結(jié)構(gòu)體的指針,因?yàn)榭战涌趯?shí)際上是具有兩個(gè)指針的結(jié)構(gòu)的語法糖:第一個(gè)指向有關(guān)類型的信息,第二個(gè)指向值。
type eface struct { _type *_type data unsafe.Pointer }
我們可以使用結(jié)構(gòu)體中字段偏移量來直接尋址該值的字段 B。
新代碼如下。
var unsafeCache = make(map[reflect.Type]uintptr) type intface struct { typ unsafe.Pointer value unsafe.Pointer } func populateStructUnsafe(in interface{}) error { typ := reflect.TypeOf(in) offset, ok := unsafeCache[typ] if !ok { if typ.Kind() != reflect.Ptr { return fmt.Errorf("you must pass in a pointer") } if typ.Elem().Kind() != reflect.Struct { return fmt.Errorf("you must pass in a pointer to a struct") } f, ok := typ.Elem().FieldByName("B") if !ok { return fmt.Errorf("struct does not have field B") } if f.Type.Kind() != reflect.Int { return fmt.Errorf("field B should be an int") } offset = f.Offset unsafeCache[typ] = offset } structPtr := (*intface)(unsafe.Pointer(&in)).value *(*int)(unsafe.Pointer(uintptr(structPtr) + offset)) = 42 return nil }
新的基準(zhǔn)測試表明這將更快。
BenchmarkPopulateUnsafe-16 62726018 19.5 ns/op 0 B/op 0 allocs/op
優(yōu)化三:更改緩存 key 類型
還能讓它走得更快嗎?如果我們對 CPU 進(jìn)行采樣,將會看到大部分時(shí)間都用于訪問 map,它還會顯示 map 訪問在調(diào)用 runtime.interhash 和 runtime.interequal。這些是用于 hash 接口并檢查它們是否相等的函數(shù)。也許使用更簡單的 key 會加快速度?我們可以使用來自接口的類型信息的地址,而不是 reflect.Type 本身。
var unsafeCache2 = make(map[uintptr]uintptr) func populateStructUnsafe2(in interface{}) error { inf := (*intface)(unsafe.Pointer(&in)) offset, ok := unsafeCache2[uintptr(inf.typ)] if !ok { typ := reflect.TypeOf(in) if typ.Kind() != reflect.Ptr { return fmt.Errorf("you must pass in a pointer") } if typ.Elem().Kind() != reflect.Struct { return fmt.Errorf("you must pass in a pointer to a struct") } f, ok := typ.Elem().FieldByName("B") if !ok { return fmt.Errorf("struct does not have field B") } if f.Type.Kind() != reflect.Int { return fmt.Errorf("field B should be an int") } offset = f.Offset unsafeCache2[uintptr(inf.typ)] = offset } *(*int)(unsafe.Pointer(uintptr(inf.value) + offset)) = 42 return nil }
這是新版本的基準(zhǔn)測試結(jié)果,它又快了很多。
BenchmarkPopulateUnsafe2-16 230836136 5.16 ns/op 0 B/op 0 allocs/op
優(yōu)化四:引入描述符
還能更快嗎?通常如果我們要將數(shù)據(jù) unmarshaling 到結(jié)構(gòu)體中,它總是相同的結(jié)構(gòu)。因此,我們可以將功能一分為二,其中一個(gè)函數(shù)用于檢查結(jié)構(gòu)是否符合要求并返回一個(gè)描述符,另外一個(gè)函數(shù)則可以在之后的填充調(diào)用中使用該描述符。
以下是我們的新代碼版本。調(diào)用者應(yīng)該在初始化時(shí)調(diào)用describeType函數(shù)以獲得一個(gè)typeDescriptor,之后調(diào)用populateStructUnsafe3函數(shù)時(shí)會用到它。在這個(gè)非常簡單的例子中,typeDescriptor只是結(jié)構(gòu)體中B字段的偏移量。
type typeDescriptor uintptr func describeType(in interface{}) (typeDescriptor, error) { typ := reflect.TypeOf(in) if typ.Kind() != reflect.Ptr { return 0, fmt.Errorf("you must pass in a pointer") } if typ.Elem().Kind() != reflect.Struct { return 0, fmt.Errorf("you must pass in a pointer to a struct") } f, ok := typ.Elem().FieldByName("B") if !ok { return 0, fmt.Errorf("struct does not have field B") } if f.Type.Kind() != reflect.Int { return 0, fmt.Errorf("field B should be an int") } return typeDescriptor(f.Offset), nil } func populateStructUnsafe3(in interface{}, ti typeDescriptor) error { structPtr := (*intface)(unsafe.Pointer(&in)).value *(*int)(unsafe.Pointer(uintptr(structPtr) + uintptr(ti))) = 42 return nil }
以下是如何使用describeType調(diào)用的新基準(zhǔn)測試。
func BenchmarkPopulateUnsafe3(b *testing.B) { b.ReportAllocs() var m SimpleStruct descriptor, err := describeType((*SimpleStruct)(nil)) if err != nil { b.Fatal(err) } for i := 0; i < b.N; i++ { if err := populateStructUnsafe3(&m, descriptor); err != nil { b.Fatal(err) } if m.B != 42 { b.Fatalf("unexpected value %d for B", m.B) } } }
現(xiàn)在基準(zhǔn)測試結(jié)果變得相當(dāng)快。
BenchmarkPopulateUnsafe3-16 1000000000 0.359 ns/op 0 B/op 0 allocs/op
這有多棒?如果我們以文章開頭原始的 populateStruct 函數(shù)編寫基準(zhǔn)測試,可以看到在不使用反射的情況下,填充這個(gè)結(jié)構(gòu)體的速度有多快。
BenchmarkPopulate-16 1000000000 0.234 ns/op 0 B/op 0 allocs/op
不出所料,這甚至比我們最好的基于反射的版本還要快一點(diǎn),但它也沒有快太多。
總結(jié)
反射并不一定很慢,但是你必須付出相當(dāng)大的努力,通過運(yùn)用 Go 內(nèi)部機(jī)理知識,在你的代碼中隨意撒上不安全的味道 ,以使其真正加速。
到此這篇關(guān)于詳解如何讓Go語言中的反射加快的文章就介紹到這了,更多相關(guān)Go語言 反射內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
go語言實(shí)現(xiàn)兩個(gè)協(xié)程交替打印
這篇文章主要介紹了go語言實(shí)現(xiàn)兩個(gè)協(xié)程交替打印,文章主要分享了兩種方法使用兩個(gè)channel和使用一個(gè)channel,內(nèi)容介紹詳細(xì)具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-03-03Golang?errgroup?設(shè)計(jì)及實(shí)現(xiàn)原理解析
這篇文章主要為大家介紹了Golang?errgroup?設(shè)計(jì)及實(shí)現(xiàn)原理解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08