欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

深入理解go?reflect反射慢的原因

 更新時(shí)間:2023年01月10日 11:42:37   作者:eleven26  
我們選擇?go?語言的一個(gè)重要原因是,它有非常高的性能。但是它反射的性能卻一直為人所詬病,本篇文章就來看看?go?反射的性能問題,感興趣的可以了解一下

我們選擇 go 語言的一個(gè)重要原因是,它有非常高的性能。但是它反射的性能卻一直為人所詬病,本篇文章就來看看 go 反射的性能問題。

go 的性能測(cè)試

在開始之前,有必要先了解一下 go 的性能測(cè)試。在 go 里面進(jìn)行性能測(cè)試很簡(jiǎn)單,只需要在測(cè)試函數(shù)前面加上 Benchmark 前綴, 然后在函數(shù)體里面使用 b.N 來進(jìn)行循環(huán),就可以得到每次循環(huán)的耗時(shí)。如下面這個(gè)例子:

func BenchmarkNew(b *testing.B) {
   b.ReportAllocs()
   for i := 0; i < b.N; i++ {
      New()
   }
}

我們可以使用命令 go test -bench=. reflect_test.go 來運(yùn)行這個(gè)測(cè)試函數(shù),又或者如果使用 goland 的話,直接點(diǎn)擊運(yùn)行按鈕就可以了。

說明:

  • *_test.go 文件中 Benchmark* 前綴函數(shù)是性能測(cè)試函數(shù),它的參數(shù)是 *testing.B 類型。
  • b.ReportAllocs():報(bào)告內(nèi)存分配次數(shù),這是一個(gè)非常重要的指標(biāo),因?yàn)?strong>內(nèi)存分配相比單純的 CPU 計(jì)算是比較耗時(shí)的操作。在性能測(cè)試中,我們需要關(guān)注內(nèi)存分配次數(shù),以及每次內(nèi)存分配的大小。
  • b.N:是一個(gè)循環(huán)次數(shù),每次循環(huán)都會(huì)執(zhí)行 New() 函數(shù),然后記錄下來每次循環(huán)的耗時(shí)。

go 里面很多優(yōu)化都致力于減少內(nèi)存分配,減少內(nèi)存分配很多情況下都可以提高性能。

輸出:

BenchmarkNew-20    1000000000    0.1286 ns/op   0 B/op   0 allocs/op

輸出說明:

  • BenchmarkNew-20BenchmarkNew 是測(cè)試函數(shù)名,-20 是 CPU 核數(shù)。
  • 1000000000:循環(huán)次數(shù)。
  • 0.1286 ns/op:每次循環(huán)的耗時(shí),單位是納秒。這里表示每次循環(huán)耗時(shí) 0.1286 納秒。
  • 0 B/op:每次循環(huán)內(nèi)存分配的大小,單位是字節(jié)。這里表示每次循環(huán)沒有分配內(nèi)存。
  • 0 allocs/op:每次循環(huán)內(nèi)存分配的次數(shù)。這里表示每次循環(huán)沒有分配內(nèi)存。

go 反射慢的原因

動(dòng)態(tài)語言的靈活性是以犧牲性能為代價(jià)的,go 語言也不例外,go 的 interface{} 提供了一定的靈活性,但是處理 interface{} 的時(shí)候就要有一些性能上的損耗了。

我們都知道,go 是一門靜態(tài)語言,這意味著我們?cè)诰幾g的時(shí)候就知道了所有的類型,而不是在運(yùn)行時(shí)才知道類型。 但是 go 里面有一個(gè) interface{} 類型,它可以表示任意類型,這就意味著我們可以在運(yùn)行時(shí)才知道類型。 但本質(zhì)上,interface{} 類型還是靜態(tài)類型,只不過它的類型和值是動(dòng)態(tài)的。 在 interface{} 類型里面,存儲(chǔ)了兩個(gè)指針,一個(gè)指向類型信息,一個(gè)指向值信息。具體可參考《go interface 設(shè)計(jì)與實(shí)現(xiàn)》。

go interface{} 帶來的靈活性

有了 interface{} 類型,讓 go 也擁有了動(dòng)態(tài)語言的特性,比如,定義一個(gè)函數(shù),它的參數(shù)是 interface{} 類型, 那么我們就可以傳入任意類型的值給這個(gè)函數(shù)。比如下面這個(gè)函數(shù)(做任意整型的加法,返回 int64 類型):

func convert(i interface{}) int64 {
   typ := reflect.TypeOf(i)
   switch typ.Kind() {
   case reflect.Int:
      return int64(i.(int))
   case reflect.Int8:
      return int64(i.(int8))
   case reflect.Int16:
      return int64(i.(int16))
   case reflect.Int32:
      return int64(i.(int32))
   case reflect.Int64:
      return i.(int64)
   default:
      panic("not support")
   }
}

func add(a, b interface{}) int64 {
   return convert(a) + convert(b)
}

說明:

  • convert() 函數(shù):將 interface{} 類型轉(zhuǎn)換為 int64 類型。對(duì)于非整型的類型,會(huì) panic。(當(dāng)然不是很嚴(yán)謹(jǐn),還沒涵蓋 uint* 類型)
  • add() 函數(shù):做任意整型的加法,返回 int64 類型。

相比之下,如果是確定的類型,我們根本不需要判斷類型,直接相加就可以了:

func add1(a, b int64) int64 {
   return a + b
}

我們可以通過以下的 benchmark 來對(duì)比一下:

func BenchmarkAdd(b *testing.B) {
   b.ReportAllocs()
   for i := 0; i < b.N; i++ {
      add(1, 2)
   }
}

func BenchmarkAdd1(b *testing.B) {
   b.ReportAllocs()
   for i := 0; i < b.N; i++ {
      add1(1, 2)
   }
}

結(jié)果:

BenchmarkAdd-12         179697526                6.667 ns/op           0 B/op          0 allocs/op
BenchmarkAdd1-12        1000000000               0.2353 ns/op          0 B/op          0 allocs/op

我們可以看到非常明顯的性能差距,add() 要比 add1() 慢了非常多,而且這還只是做了一些簡(jiǎn)單的類型判斷及類型轉(zhuǎn)換的情況下。

go 靈活性的代價(jià)(慢的原因)

通過這個(gè)例子我們知道,go 雖然通過 interface{} 為我們提供了一定的靈活性支持,但是使用這種動(dòng)態(tài)的特性是有一定代價(jià)的,比如:

  • 我們?cè)谶\(yùn)行時(shí)才知道類型,那么我們就需要在運(yùn)行時(shí)去做類型判斷(也就是通過反射),這種判斷會(huì)有一定開銷(本來是確定的一種類型,但是現(xiàn)在可能要在 20 多個(gè)類型中匹配才能確定它的類型是什么)。同時(shí),判斷到屬于某一類型之后,往往需要轉(zhuǎn)換為具體的類型,這也是一種開銷。
  • 同時(shí),我們可能需要去做一些屬性、方法的查找等操作(Field, FieldByName, Method, MethodByName),這些操作都是在運(yùn)行時(shí)做的,所以會(huì)有一定的性能損耗。
  • 另外,在做屬性、方法之類的查找的時(shí)候,查找性能取決于屬性、方法的數(shù)量,如果屬性、方法的數(shù)量很多,那么查找性能就會(huì)相對(duì)慢。通過 index (Field, Method)查找相比通過 name (FieldByName, MethodByName)查找快很多,后者有內(nèi)存分配的操作
  • 在我們通過反射來做這些操作的時(shí)候,多出了很多操作,比如,簡(jiǎn)單的兩個(gè) int 類型相加,本來可以直接相加。但是通過反射,我們不得不先根據(jù) interface{} 創(chuàng)建一個(gè)反射對(duì)象,然后再做類型判斷,再做類型轉(zhuǎn)換,最后再做加法。

總的來說,go 的 interface{} 類型雖然給我們提供了一定的靈活性,讓開發(fā)者也可以在 go 里面實(shí)現(xiàn)一些動(dòng)態(tài)語言的特性, 但是這種靈活性是以犧牲一定的性能來作為代價(jià)的,它會(huì)讓一些簡(jiǎn)單的操作變得復(fù)雜,一方面生成的編譯指令會(huì)多出幾十倍,另一方面也有可能在這過程有內(nèi)存分配的發(fā)生(比如 FieldByName)。

慢是相對(duì)的

從上面的例子中,我們發(fā)現(xiàn) go 的反射好像慢到了讓人無法忍受的地步,然后就有人提出了一些解決方案, 比如:通過代碼生成的方式避免運(yùn)行時(shí)的反射操作,從而提高性能。比如 easyjson

但是這類方案都會(huì)讓代碼變得繁雜起來。我們需要權(quán)衡之后再做決定。為什么呢?因?yàn)榉瓷潆m然慢,但我們要知道的是,如果我們的應(yīng)用中有網(wǎng)絡(luò)調(diào)用,任何一次網(wǎng)絡(luò)調(diào)用的時(shí)間往往都不會(huì)少于 1ms,而這 1ms 足夠 go 做很多次反射操作了。這給我們什么啟示呢?如果我們不是做中間件或者是做一些高性能的服務(wù),而是做一些 web 應(yīng)用,那么我們可以考慮一下性能瓶頸是不是在反射這里,如果是,那么我們就可以考慮一下代碼生成的方式來提高性能,如果不是,那么我們真的需要犧牲代碼的可維護(hù)性、可讀性來提高反射的性能嗎??jī)?yōu)化幾個(gè)慢查詢帶來的收益是不是更高呢?

go 反射性能優(yōu)化

如果可以的話,最好的優(yōu)化就是不要用反射。

通過代碼生成的方式避免序列化和反序列化時(shí)的反射操作

這里以 easyjson 為例,我們來看一下它是怎么做的。假設(shè)我們有如下結(jié)構(gòu)體,我們需要對(duì)其進(jìn)行 json 序列化/反序列化:

// person.go
type Person struct {
   Name string `json:"name"`
   Age  int    `json:"age"`
}

使用 easyjson 的話,我們需要為結(jié)構(gòu)體生成代碼,這里我們使用 easyjson 的命令行工具來生成代碼:

easyjson -all person.go

這樣,我們就會(huì)在當(dāng)前目錄下生成 person_easyjson.go 文件,里面包含了 MarshalJSONUnmarshalJSON 方法,這兩個(gè)方法就是我們需要的序列化和反序列化方法。不同于標(biāo)準(zhǔn)庫里面的 json.Marshaljson.Unmarshal,這兩個(gè)方法是不需要反射的,它們的性能會(huì)比標(biāo)準(zhǔn)庫的方法要好很多。

func easyjsonDb0593a3EncodeGithubComGinGonicGinCEasy(out *jwriter.Writer, in Person) {
   out.RawByte('{')
   first := true
   _ = first
   {
      const prefix string = ","name":"
      out.RawString(prefix[1:])
      out.String(string(in.Name))
   }
   {
      const prefix string = ","age":"
      out.RawString(prefix)
      out.Int(int(in.Age))
   }
   out.RawByte('}')
}

// MarshalJSON supports json.Marshaler interface
func (v Person) MarshalJSON() ([]byte, error) {
   w := jwriter.Writer{}
   easyjsonDb0593a3EncodeGithubComGinGonicGinCEasy(&w, v)
   return w.Buffer.BuildBytes(), w.Error
}

我們看到,我們對(duì) Person 的序列化操作現(xiàn)在只需要幾行代碼就可以完成了,但是也有很明顯的缺點(diǎn),生成的代碼會(huì)很多。

性能差距:

goos: darwin
goarch: amd64
pkg: github.com/gin-gonic/gin/c/easy
cpu: Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz
BenchmarkJson
BenchmarkJson-12            3680560          305.9 ns/op      152 B/op         2 allocs/op
BenchmarkEasyJson
BenchmarkEasyJson-12       16834758           71.37 ns/op         128 B/op         1 allocs/op

我們可以看到,使用 easyjson 生成的代碼,序列化的性能比標(biāo)準(zhǔn)庫的方法要好很多,好了 4 倍以上。

反射結(jié)果緩存

這種方法適用于需要根據(jù)名稱查找結(jié)構(gòu)體字段或者查找方法的場(chǎng)景。

假設(shè)我們有一個(gè)結(jié)構(gòu)體 Person,其中有 5 個(gè)方法,M1、M2、M3M4、M5,我們需要通過名稱來查找其中的方法,那么我們可以使用 reflect 包來實(shí)現(xiàn):

p := &Person{}
v := reflect.ValueOf(p)
v.MethodByName("M4")

這是很容易想到的辦法,但是性能如何呢?通過性能測(cè)試,我們可以看到,這種方式的性能是非常差的:

func BenchmarkMethodByName(b *testing.B) {
   p := &Person{}
   v := reflect.ValueOf(p)

   b.ReportAllocs()
   for i := 0; i < b.N; i++ {
      v.MethodByName("M4")
   }
}

結(jié)果:

BenchmarkMethodByName-12         5051679               237.1 ns/op           120 B/op          3 allocs/op

相比之下,我們?nèi)绻褂盟饕齺慝@取其中的方法的話,性能會(huì)好很多:

func BenchmarkMethod(b *testing.B) {
   p := &Person{}
   v := reflect.ValueOf(p)

   b.ReportAllocs()
   for i := 0; i < b.N; i++ {
      v.Method(3)
   }
}

結(jié)果:

BenchmarkMethod-12              200091475                5.958 ns/op           0 B/op          0 allocs/op

我們可以看到兩種性能相差幾十倍。那么我們是不是可以通過 Method 方法來替代 MethodByName 從而獲得更好的性能呢?答案是可以的,我們可以緩存 MethodByName 的結(jié)果(就是方法名對(duì)應(yīng)的下標(biāo)),下次通過反射獲取對(duì)應(yīng)方法的時(shí)候直接通過這個(gè)下標(biāo)來獲取:

這里需要通過 reflect.Type 的 MethodByName 來獲取反射的方法對(duì)象。

// 緩存方法名對(duì)應(yīng)的方法下標(biāo)
var indexCache = make(map[string]int)

func methodIndex(p interface{}, method string) int {
   if _, ok := indexCache[method]; !ok {
      m, ok := reflect.TypeOf(p).MethodByName(method)
      if !ok {
         panic("method not found!")
      }

      indexCache[method] = m.Index
   }

   return indexCache[method]
}

性能測(cè)試:

func BenchmarkMethodByNameCache(b *testing.B) {
   p := &Person{}
   v := reflect.ValueOf(p)

   b.ReportAllocs()
   var idx int
   for i := 0; i < b.N; i++ {
      idx = methodIndex(p, "M4")
      v.Method(idx)
   }
}

結(jié)果:

// 相比原來的 MethodByName 快了將近 20 倍
BenchmarkMethodByNameCache-12           86208202                13.65 ns/op            0 B/op          0 allocs/op
BenchmarkMethodByName-12                 5082429               235.9 ns/op           120 B/op          3 allocs/op

跟這個(gè)例子類似的是 Field/FieldByName 方法,可以采用同樣的優(yōu)化方式。這個(gè)可能是更加常見的操作,反序列化可能需要通過字段名查找字段,然后進(jìn)行賦值。

使用類型斷言代替反射

在實(shí)際使用中,如果只是需要進(jìn)行一些簡(jiǎn)單的類型判斷的話,比如判斷是否實(shí)現(xiàn)某一個(gè)接口,那么可以使用類型斷言來實(shí)現(xiàn):

type Talk interface {
   Say()
}

type person struct {
}

func (p person) Say() {
}

func BenchmarkReflectCall(b *testing.B) {
   p := person{}
   v := reflect.ValueOf(p)

   for i := 0; i < b.N; i++ {
      idx := methodIndex(&p, "Say")
      v.Method(idx).Call(nil)
   }
}

func BenchmarkAssert(b *testing.B) {
   p := person{}

   for i := 0; i < b.N; i++ {
      var inter interface{} = p
      if v, ok := inter.(Talk); ok {
         v.Say()
      }
   }
}

結(jié)果:

goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz
BenchmarkReflectCall-12          6906339               173.1 ns/op
BenchmarkAssert-12              171741784                6.922 ns/op

在這個(gè)例子中,我們就算使用了緩存版本的反射,性能也跟類型斷言差了將近 25 倍。

因此,在我們使用反射之前,我們需要先考慮一下是否可以通過類型斷言來實(shí)現(xiàn),如果可以的話,那么就不需要使用反射了。

總結(jié)

go 提供了性能測(cè)試的工具,我們可以通過 go test -bench=. 這種命令來進(jìn)行性能測(cè)試,運(yùn)行命令之后,文件夾下的測(cè)試文件中的 Benchmark* 函數(shù)會(huì)被執(zhí)行。

性能測(cè)試的結(jié)果中,除了平均執(zhí)行耗時(shí)之外,還有內(nèi)存分配的次數(shù)和內(nèi)存分配的字節(jié)數(shù),這些都是我們需要關(guān)注的指標(biāo)。其中內(nèi)存分配的次數(shù)和內(nèi)存分配的字節(jié)數(shù)是可以通過 b.ReportAllocs() 來進(jìn)行統(tǒng)計(jì)的。內(nèi)存分配的次數(shù)和內(nèi)存分配的字節(jié)數(shù)越少,性能越好。

反射雖然慢,但是也帶來了一定的靈活性,它的慢主要由以下幾個(gè)方面的原因造成的:

  • 運(yùn)行時(shí)需要進(jìn)行類型判斷,相比確定的類型,運(yùn)行時(shí)可能需要在 20 多種類型中進(jìn)行判斷。
  • 類型判斷之后,往往需要將 interface{} 轉(zhuǎn)換為具體的類型,這個(gè)轉(zhuǎn)換也是需要消耗一定時(shí)間的。
  • 方法、字段的查找也是需要消耗一定時(shí)間的。尤其是 FieldByName, MethodByName 這種方法,它們需要遍歷所有的字段和方法,然后進(jìn)行比較,這個(gè)比較的過程也是需要消耗一定時(shí)間的。而且這個(gè)過程還需要分配內(nèi)存,這會(huì)進(jìn)一步降低性能。

慢不慢是一個(gè)相對(duì)的概念,如果我們的應(yīng)用大部分時(shí)間是在 IO 等待,那么反射的性能大概率不會(huì)成為瓶頸。優(yōu)化其他地方可能會(huì)帶來更大的收益,同時(shí)也可以在不影響代碼可維護(hù)性的前提下,使用一些時(shí)空復(fù)雜度更低的反射方法,比如使用 Field 代替 FieldByName 等。

如果可以的話,盡量不使用反射就是最好的優(yōu)化。

反射的一些性能優(yōu)化方式有如下幾種(不完全,需要根據(jù)實(shí)際情況做優(yōu)化):

  • 使用生成代碼的方式,生成特定的序列化和反序列化方法,這樣就可以避免反射的開銷。
  • 將第一次反射拿到的結(jié)果緩存起來,這樣如果后續(xù)需要反射的話,就可以直接使用緩存的結(jié)果,避免反射的開銷。(空間換時(shí)間
  • 如果只是需要進(jìn)行簡(jiǎn)單的類型判斷,可以先考慮一下類型斷言能不能實(shí)現(xiàn)我們想要的效果,它相比反射的開銷要小很多。

反射是一個(gè)很龐大的話題,這里只是簡(jiǎn)單的介紹了一小部分反射的性能問題,討論了一些可行的優(yōu)化方案,但是每個(gè)人使用反射的場(chǎng)景都不一樣,所以需要根據(jù)實(shí)際情況來做優(yōu)化。

到此這篇關(guān)于深入理解go reflect反射慢的原因的文章就介紹到這了,更多相關(guān)go reflect反射慢內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • 基于微服務(wù)框架go-micro開發(fā)gRPC應(yīng)用程序

    基于微服務(wù)框架go-micro開發(fā)gRPC應(yīng)用程序

    這篇文章介紹了基于微服務(wù)框架go-micro開發(fā)gRPC應(yīng)用程序的方法,文中通過示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2022-07-07
  • go語言中strings包的用法匯總

    go語言中strings包的用法匯總

    Golang語言 strings標(biāo)準(zhǔn)庫包主要涉及字符串的基本操作,下面我們來詳細(xì)分析下吧
    2018-10-10
  • 淺談go中切片比數(shù)組好用在哪

    淺談go中切片比數(shù)組好用在哪

    數(shù)組和切片都是常見的數(shù)據(jù)結(jié)構(gòu),本文將介紹Go語言中數(shù)組和切片的基本概念,同時(shí)詳細(xì)探討切片的優(yōu)勢(shì),感興趣的可以了解下
    2023-06-06
  • 關(guān)于golang監(jiān)聽rabbitmq消息隊(duì)列任務(wù)斷線自動(dòng)重連接的問題

    關(guān)于golang監(jiān)聽rabbitmq消息隊(duì)列任務(wù)斷線自動(dòng)重連接的問題

    這篇文章主要介紹了golang監(jiān)聽rabbitmq消息隊(duì)列任務(wù)斷線自動(dòng)重連接,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2022-03-03
  • Golang 語言極簡(jiǎn)類型轉(zhuǎn)換庫cast的使用詳解

    Golang 語言極簡(jiǎn)類型轉(zhuǎn)換庫cast的使用詳解

    本文我們通過 cast.ToString() 函數(shù)的使用,簡(jiǎn)單介紹了cast 的使用方法,除此之外,它還支持很多其他類型,在這沒有多多介紹,對(duì)Golang 類型轉(zhuǎn)換庫 cast相關(guān)知識(shí)感興趣的朋友一起看看吧
    2021-11-11
  • 深入了解Golang網(wǎng)絡(luò)編程N(yùn)et包的使用

    深入了解Golang網(wǎng)絡(luò)編程N(yùn)et包的使用

    net包主要是增加?context?控制,封裝了一些不同的連接類型以及DNS?查找等等,同時(shí)在有需要的地方引入?goroutine?提高處理效率。本文主要和大家分享下在Go中網(wǎng)絡(luò)編程的實(shí)現(xiàn),需要的可以參考一下
    2022-07-07
  • golang獲取客戶端ip的實(shí)現(xiàn)

    golang獲取客戶端ip的實(shí)現(xiàn)

    本文主要介紹了golang獲取客戶端ip的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-07-07
  • vscode配置go開發(fā)環(huán)境的實(shí)戰(zhàn)過程

    vscode配置go開發(fā)環(huán)境的實(shí)戰(zhàn)過程

    vscode配置go的開發(fā)環(huán)境很簡(jiǎn)單,下面這篇文章主要給大家介紹了關(guān)于vscode配置go開發(fā)環(huán)境的實(shí)戰(zhàn)過程,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2022-06-06
  • go+redis實(shí)現(xiàn)消息隊(duì)列發(fā)布與訂閱的詳細(xì)過程

    go+redis實(shí)現(xiàn)消息隊(duì)列發(fā)布與訂閱的詳細(xì)過程

    這篇文章主要介紹了go+redis實(shí)現(xiàn)消息隊(duì)列發(fā)布與訂閱,redis做消息隊(duì)列的缺點(diǎn):沒有持久化,一旦消息沒有人消費(fèi),積累到一定程度后就會(huì)丟失,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2022-09-09
  • golang執(zhí)行命令獲取執(zhí)行結(jié)果狀態(tài)(推薦)

    golang執(zhí)行命令獲取執(zhí)行結(jié)果狀態(tài)(推薦)

    這篇文章主要介紹了golang執(zhí)行命令獲取執(zhí)行結(jié)果狀態(tài)的相關(guān)知識(shí),非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友參考下吧
    2019-11-11

最新評(píng)論