現在的一些流行設計思想需要建立在反射基礎上,如控制反轉(Inversion Of Control,IOC)和依賴注入(Dependency Injection,DI)。Go語言中非常有名的 Web 框架 martini(https://github.com/go-martini/martini)就是通過依賴注入技術進行中介軟體的實現,例如使用 martini 框架搭建的 http 的伺服器如下:
package main
import "github.com/go-martini/martini"
func main() {
m := martini.Classic()
m.Get("/", func() string {
return "Hello world!"
})
m.Run()
}
第 7 行,響應路徑
/
的程式碼使用一個閉包實現。如果希望獲得 Go語言中提供的請求和響應介面,可以直接修改為:
m.Get("/", func(res http.ResponseWriter, req *http.Request) string {
// 響應處理程式碼……
})
martini 的底層會自動通過識別 Get 獲得的閉包引數情況,通過動態反射呼叫這個函數並傳入需要的引數。martini 的設計廣受好評,但同時也有人指出,其執行效率較低。其中最主要的因素是大量使用了反射。
雖然一般情況下,I/O 的延遲遠遠大於反射程式碼所造成的延遲。但是,更低的響應速度和更低的 CPU 占用依然是 Web 伺服器追求的目標。因此,反射在帶來靈活性的同時,也帶上了效能低下的桎梏。
要用好反射這把雙刃劍,就需要詳細了解反射的效能。下面的一些基準測試從多方面對比了原生呼叫和反射呼叫的區別。
1) 結構體成員賦值對比
反射經常被使用在結構體上,因此結構體的成員存取效能就成為了關注的重點。下面例子中使用一個被範例化的結構體,存取它的成員,然後使用 Go語言的基準化測試可以迅速測試出結果。
反射效能測試的完整程式碼位於
./src/chapter12/reflecttest/reflect_test.go
,下面是對各個部分的詳細說明。
本套教學所有原始碼下載地址:https://pan.baidu.com/s/1ORFVTOLEYYqDhRzeq0zIiQ 提取密碼:hfyf
原生結構體的賦值過程:
// 宣告一個結構體, 擁有一個欄位
type data struct {
Hp int
}
func BenchmarkNativeAssign(b *testing.B) {
// 範例化結構體
v := data{Hp: 2}
// 停止基準測試的計時器
b.StopTimer()
// 重置基準測試計時器資料
b.ResetTimer()
// 重新啟動基準測試計時器
b.StartTimer()
// 根據基準測試資料進行迴圈測試
for i := 0; i < b.N; i++ {
// 結構體成員賦值測試
v.Hp = 3
}
}
程式碼說明如下:
-
第 2 行,宣告一個普通結構體,擁有一個成員變數。
-
第 6 行,使用基準化測試的入口。
-
第 9 行,範例化 data 結構體,並給 Hp 成員賦值。
-
第 12~17 行,由於測試的重點必須放在賦值上,因此需要極大程度地降低其他程式碼的干擾,於是在賦值完成後,將基準測試的計時器復位並重新開始。
-
第 20 行,將基準測試提供的測試數量用於迴圈中。
-
第 23 行,測試的核心程式碼:結構體賦值。
接下來的程式碼分析使用反射存取結構體成員並賦值的過程。
func BenchmarkReflectAssign(b *testing.B) {
v := data{Hp: 2}
// 取出結構體指標的反射值物件並取其元素
vv := reflect.ValueOf(&v).Elem()
// 根據名字取結構體成員
f := vv.FieldByName("Hp")
b.StopTimer()
b.ResetTimer()
b.StartTimer()
for i := 0; i < b.N; i++ {
// 反射測試設定成員值效能
f.SetInt(3)
}
}
程式碼說明如下:
-
第 6 行,取v的地址並轉為反射值物件。此時值物件裡的型別為 *data,使用值的 Elem() 方法取元素,獲得 data 的反射值物件。
-
第 9 行,使用 FieldByName() 根據名字取出成員的反射值物件。
-
第 11~13 行,重置基準測試計時器。
-
第 18 行,使用反射值物件的 SetInt() 方法,給 data 結構的
Hp
欄位設定數值 3。
這段程式碼中使用了反射值物件的 SetInt() 方法,這個方法的原始碼如下:
func (v Value) SetInt(x int64) {
v.mustBeAssignable()
switch k := v.kind(); k {
default:
panic(&ValueError{"reflect.Value.SetInt", v.kind()})
case Int:
*(*int)(v.ptr) = int(x)
case Int8:
*(*int8)(v.ptr) = int8(x)
case Int16:
*(*int16)(v.ptr) = int16(x)
case Int32:
*(*int32)(v.ptr) = int32(x)
case Int64:
*(*int64)(v.ptr) = x
}
}
可以發現,整個設定過程都是指標轉換及賦值,沒有遍歷及記憶體操作等相對耗時的演算法。
2) 結構體成員搜尋並賦值對比
func BenchmarkReflectFindFieldAndAssign(b *testing.B) {
v := data{Hp: 2}
vv := reflect.ValueOf(&v).Elem()
b.StopTimer()
b.ResetTimer()
b.StartTimer()
for i := 0; i < b.N; i++ {
// 測試結構體成員的查詢和設定成員的效能
vv.FieldByName("Hp").SetInt(3)
}
}
這段程式碼將反射值物件的 FieldByName() 方法與 SetInt() 方法放在迴圈裡進行檢測,主要對比測試 FieldByName() 方法對效能的影響。FieldByName() 方法原始碼如下:
func (v Value) FieldByName(name string) Value {
v.mustBe(Struct)
if f, ok := v.typ.FieldByName(name); ok {
return v.FieldByIndex(f.Index)
}
return Value{}
}
底層程式碼說明如下:
-
第 3 行,通過名字查詢型別物件,這裡有一次遍歷過程。
-
第 4 行,找到型別物件後,使用 FieldByIndex() 繼續在值中查詢,這裡又是一次遍歷。
經過底層程式碼分析得出,隨著結構體欄位數量和相對位置的變化,FieldByName() 方法比較嚴重的低效率問題。
3) 呼叫函數對比
反射的函數呼叫,也是使用反射中容易忽視的效能點,下面展示對普通函數的呼叫過程。
// 一個普通函數
func foo(v int) {
}
func BenchmarkNativeCall(b *testing.B) {
for i := 0; i < b.N; i++ {
// 原生函數呼叫
foo(0)
}
}
func BenchmarkReflectCall(b *testing.B) {
// 取函數的反射值物件
v := reflect.ValueOf(foo)
b.StopTimer()
b.ResetTimer()
b.StartTimer()
for i := 0; i < b.N; i++ {
// 反射呼叫函數
v.Call([]reflect.Value{reflect.ValueOf(2)})
}
}
程式碼說明如下:
-
第 2 行,一個普通的只有一個引數的函數。
-
第 10 行,對原生函數呼叫的效能測試。
-
第 17 行,根據函數名取出反射值物件。
-
第 25 行,使用 reflect.ValueOf(2) 將 2 構造為反射值物件,因為反射函數呼叫的引數必須全是反射值物件,再使用 []reflect.Value 構造多個參數列傳給反射值物件的 Call() 方法進行呼叫。
反射函數呼叫的引數構造過程非常複雜,構建很多物件會造成很大的記憶體回收負擔。Call() 方法內部就更為複雜,需要將參數列的每個值從 reflect.Value 型別轉換為記憶體。呼叫完畢後,還要將函數返回值重新轉換為 reflect.Value 型別返回。因此,反射呼叫函數的效能堪憂。
4) 基準測試結果對比
測試結果如下:
$ go test -v -bench=.
goos: linux
goarch: amd64
BenchmarkNativeAssign-4 2000000000 0.32 ns/op
BenchmarkReflectAssign-4 300000000 4.42 ns/op
BenchmarkReflectFindFieldAndAssign-4 20000000 91.6 ns/op
BenchmarkNativeCall-4 2000000000 0.33 ns/op
BenchmarkReflectCall-4 10000000 163 ns/op
PASS
結果分析如下:
-
第 4 行,原生的結構體成員賦值,每一步操作耗時 0.32 納秒,這是參考基準。
-
第 5 行,使用反射的結構體成員賦值,操作耗時 4.42 納秒,比原生賦值多消耗 13 倍的效能。
-
第 6 行,反射查詢結構體成員且反射賦值,操作耗時 91.6 納秒,扣除反射結構體成員賦值的 4.42 納秒還富餘,效能大概是原生的 272 倍。這個測試結果與程式碼分析結果很接近。SetInt 的效能可以接受,但 FieldByName() 的效能就非常低。
-
第 7 行,原生函數呼叫,效能與原生存取結構體成員接近。
-
第 8 行,反射函數呼叫,效能差到“爆棚”,花費了 163 納秒,操作耗時比原生多消耗 494 倍。
經過基準測試結果的數值分析及對比,最終得出以下結論:
-
能使用原生程式碼時,盡量避免反射操作。
-
提前緩衝反射值物件,對效能有很大的幫助。
-
避免反射函數呼叫,實在需要呼叫時,先提前緩衝函數參數列,並且盡量少地使用返回值。