<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
最近讀到一篇關於 Go 反射的文章,作者通過反射給結構體填充欄位值的案例,充分利用 Go 的各種內在機理,逐步探討讓程式碼執行得更快的姿勢。
文章(原文地址:https://philpearl.github.io/post/aintnecessarilyslow/)非常有學習價值,故翻譯整理了下來。
不要使用反射,除非你真的需要。但是當你不使用反射時,不要認為這是因為反射很慢,它也可以很快。
反射允許你在執行時獲得有關 Go 型別的資訊。如果你曾經愚蠢地嘗試編寫 json.Unmarshal 之類的新版本,本文將探討的就是如何使用反射來填充結構體值。
我們以一個簡單的案例為切入點,定義一個結構體 SimpleStruct,它包括兩個 int 型別欄位 A 和 B。
type SimpleStruct struct { A int B int }
假如我們接收到了 JSON 資料 {"B": 42},想要對其進行解析並且將欄位 B 設定為 42。
在下文,我們將編寫一些函數來實現這一點,它們都會將 B 設定為 42。
如果我們的程式碼只適用於 SimpleStruct,這完全是不值一提的。
func populateStruct(in *SimpleStruct) { in.B = 42 }
但是,如果我們是要做一個 JSON 解析器,這意味著我們並不能提前知道結構型別。我們的解析器程式碼需要接收任何型別的資料。
在 Go 中,這通常意味著需要採用 interface{} (空介面)引數。然後我們可以使用 reflect 包檢查通過空介面引數傳入的值,檢查它是否是指向結構體的指標,找到欄位 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 }
讓我們通過基準測試看看它有多快。
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) } } }
結果如下。
BenchmarkPopulateReflect-16 15941916 68.3 ns/op 8 B/op 1 allocs/op
這是好還是壞?好吧,記憶體分配可從來不是好事。你可能想知道為什麼需要在堆上分配記憶體來將結構體欄位設定為 42(可以看這個 issue:https://github.com/golang/go/issues/2320)。但總體而言,68ns 的時間並不長。在通過網路發出任何型別的請求時間中,你可以容納很多 68ns。
我們能做得更好嗎?好吧,通常我們執行的程式不會只做一件事然後停止。他們通常一遍又一遍地做著非常相似的事情。因此,我們可以設定一些東西以使重複的事情速度變快嗎?
如果仔細檢視我們正在執行的反射檢查,我們會發現它們都取決於傳入值的型別。如果我們將型別結果快取起來,那麼對於每種型別而言,我們只會進行一次檢查。
我們再來考慮記憶體分配的問題。之前我們呼叫 Value.FieldByName 方法,實際是 Value.FieldByName 呼叫 Type.FieldByName,其呼叫 structType.FieldByName,最後呼叫 structType.Field 來引起記憶體分配的。我們可以在型別上呼叫 FieldByName 並快取一些東西來獲取 B 欄位的值嗎?實際上,如果我們快取 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 }
因為沒有任何記憶體分配,新的基準測試變得更快。
BenchmarkPopulateReflectCache-16 35881779 30.9 ns/op 0 B/op 0 allocs/op
我們能做得更好嗎?好吧,如果我們知道結構體欄位 B 的偏移量並且知道它是 int 型別,就可以將其直接寫入記憶體。我們可以從介面中恢復指向結構體的指標,因為空介面實際上是具有兩個指標的結構的語法糖:第一個指向有關型別的資訊,第二個指向值。
type eface struct { _type *_type data unsafe.Pointer }
我們可以使用結構體中欄位偏移量來直接定址該值的欄位 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 }
新的基準測試表明這將更快。
BenchmarkPopulateUnsafe-16 62726018 19.5 ns/op 0 B/op 0 allocs/op
還能讓它走得更快嗎?如果我們對 CPU 進行取樣,將會看到大部分時間都用於存取 map,它還會顯示 map 存取在呼叫 runtime.interhash 和 runtime.interequal。這些是用於 hash 介面並檢查它們是否相等的函數。也許使用更簡單的 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 }
這是新版本的基準測試結果,它又快了很多。
BenchmarkPopulateUnsafe2-16 230836136 5.16 ns/op 0 B/op 0 allocs/op
還能更快嗎?通常如果我們要將資料 unmarshaling 到結構體中,它總是相同的結構。因此,我們可以將功能一分為二,其中一個函數用於檢查結構是否符合要求並返回一個描述符,另外一個函數則可以在之後的填充呼叫中使用該描述符。
以下是我們的新程式碼版本。呼叫者應該在初始化時呼叫describeType函數以獲得一個typeDescriptor,之後呼叫populateStructUnsafe3函數時會用到它。在這個非常簡單的例子中,typeDescriptor只是結構體中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呼叫的新基準測試。
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) } } }
現在基準測試結果變得相當快。
BenchmarkPopulateUnsafe3-16 1000000000 0.359 ns/op 0 B/op 0 allocs/op
這有多棒?如果我們以文章開頭原始的 populateStruct 函數編寫基準測試,可以看到在不使用反射的情況下,填充這個結構體的速度有多快。
BenchmarkPopulate-16 1000000000 0.234 ns/op 0 B/op 0 allocs/op
不出所料,這甚至比我們最好的基於反射的版本還要快一點,但它也沒有快太多。
反射並不一定很慢,但是你必須付出相當大的努力,通過運用 Go 內部機理知識,在你的程式碼中隨意撒上不安全的味道 ,以使其真正加速。
到此這篇關於詳解如何讓Go語言中的反射加快的文章就介紹到這了,更多相關Go語言 反射內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45