Go Reflect 提高反射性能

源代码/数据集已上传到 Github - high-performance-go

high performance go - data structure

1 反射的用途

标准库 reflect 为 Go 语言提供了运行时动态获取对象的类型和值以及动态创建对象的能力。反射可以帮助抽象和简化代码,提高开发效率。

jsongorm/xorm

在 7days-golang 这个项目中,也有好几处用到了反射。在 七天用Go从零实现RPC框架 中,我们使用反射在服务端,利用接收到的二进制报文动态创建对象,例如利用反射实现函数的动态调用。在 7天用Go从零实现ORM框架GeeORM 中,我们使用反射,实现了结构体(struct)类型和数据库表名的映射,结构体字段和数据库字段的映射。同样利用反射动态创建对象的能力,将数据库中查询到的记录转换为 Go 语言中的对象。

2 反射如何简化代码

接下来呢,我们利用反射实现一个简单的功能,来看看反射如何帮助我们简化代码的。

假设有一个配置类 Config,每个字段是一个配置项。为了简化实现,假设字段均为 string 类型:

1
2
3
4
5
6
type Config struct {
Name string `json:"server-name"`
IP string `json:"server-ip"`
URL string `json:"server-url"`
Timeout string `json:"timeout"`
}
json-CONFIG_

最终的对应结果如下:

1
2
3
4
5
6
type Config struct {
Name string `json:"server-name"` // CONFIG_SERVER_NAME
IP string `json:"server-ip"` // CONFIG_SERVER_IP
URL string `json:"server-url"` // CONFIG_SERVER_URL
Timeout string `json:"timeout"` // CONFIG_TIMEOUT
}
switch caseif elseConfigjson

这个时候,就有了 reflect 的用武之地了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func readConfig() *Config {
// read from xxx.json,省略
config := Config{}
typ := reflect.TypeOf(config)
value := reflect.Indirect(reflect.ValueOf(&config))
for i := 0; i < typ.NumField(); i++ {
f := typ.Field(i)
if v, ok := f.Tag.Lookup("json"); ok {
key := fmt.Sprintf("CONFIG_%s", strings.ReplaceAll(strings.ToUpper(v), "-", "_"))
if env, exist := os.LookupEnv(key); exist {
value.FieldByName(f.Name).Set(reflect.ValueOf(env))
}
}
}
return &config
}

func main() {
os.Setenv("CONFIG_SERVER_NAME", "global_server")
os.Setenv("CONFIG_SERVER_IP", "10.0.0.1")
os.Setenv("CONFIG_SERVER_URL", "geektutu.com")
c := readConfig()
fmt.Printf("%+v", c)
}

实现逻辑其实是非常简单的:

ConfigTag

运行该程序,输出为:

1
&{Name:global_server IP:10.0.0.1 URL:geektutu.com Timeout:}
Config

3 反射的性能

毫无疑问的是,反射会增加额外的代码指令,对性能肯定会产生影响的。具体影响有多大,我们可以使用 Benchmark 来测试一番。

3.1 创建对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func BenchmarkNew(b *testing.B) {
var config *Config
for i := 0; i < b.N; i++ {
config = new(Config)
}
_ = config
}

func BenchmarkReflectNew(b *testing.B) {
var config *Config
typ := reflect.TypeOf(Config{})
b.ResetTimer()
for i := 0; i < b.N; i++ {
config, _ = reflect.New(typ).Interface().(*Config)
}
_ = config
}

测试结果如下:

1
2
3
4
5
6
7
8
$ go test -bench .          
goos: darwin
goarch: amd64
pkg: example/hpg-reflect
BenchmarkNew-8 26478909 40.9 ns/op
BenchmarkReflectNew-8 18983700 62.1 ns/op
PASS
ok example/hpg-reflect 2.382s
new

3.2 修改字段的值

FieldByNameFieldFieldByName
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
func BenchmarkSet(b *testing.B) {
config := new(Config)
b.ResetTimer()
for i := 0; i < b.N; i++ {
config.Name = "name"
config.IP = "ip"
config.URL = "url"
config.Timeout = "timeout"
}
}

func BenchmarkReflect_FieldSet(b *testing.B) {
typ := reflect.TypeOf(Config{})
ins := reflect.New(typ).Elem()
b.ResetTimer()
for i := 0; i < b.N; i++ {
ins.Field(0).SetString("name")
ins.Field(1).SetString("ip")
ins.Field(2).SetString("url")
ins.Field(3).SetString("timeout")
}
}

func BenchmarkReflect_FieldByNameSet(b *testing.B) {
typ := reflect.TypeOf(Config{})
ins := reflect.New(typ).Elem()
b.ResetTimer()
for i := 0; i < b.N; i++ {
ins.FieldByName("Name").SetString("name")
ins.FieldByName("IP").SetString("ip")
ins.FieldByName("URL").SetString("url")
ins.FieldByName("Timeout").SetString("timeout")
}
}

测试结果如下:

1
2
3
4
5
6
7
8
9
$ go test -bench="Set$" .          
goos: darwin
goarch: amd64
pkg: example/hpg-reflect
BenchmarkSet-8 1000000000 0.302 ns/op
BenchmarkReflect_FieldSet-8 33913672 34.5 ns/op
BenchmarkReflect_FieldByNameSet-8 3775234 316 ns/op
PASS
ok example/hpg-reflect 3.066s
  • 三种场景下,对象已经提前创建好,测试的均为给字段赋值所消耗的时间。
  • 普通的赋值操作,每次耗时约为 0.3 ns,通过下标找到对应的字段再赋值,每次耗时约为 30 ns,通过名称找到对应字段再赋值,每次耗时约为 300 ns。
ConfigFieldByNameField

3.3 FieldByName 和 Field 性能差距

FieldByNameFieldFieldByName
  • reflect/value.go
1
2
3
4
5
6
7
8
9
10
// FieldByName returns the struct field with the given name.
// It returns the zero Value if no field was found.
// It panics if v's Kind is not struct.
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{}
}
  • reflect/type.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func (t *rtype) FieldByName(name string) (StructField, bool) {
if t.Kind() != Struct {
panic("reflect: FieldByName of non-struct type")
}
tt := (*structType)(unsafe.Pointer(t))
return tt.FieldByName(name)
}

// FieldByName returns the struct field with the given name
// and a boolean to indicate if the field was found.
func (t *structType) FieldByName(name string) (f StructField, present bool) {
// Quick check for top-level name, or struct without embedded fields.
hasEmbeds := false
if name != "" {
for i := range t.fields {
tf := &t.fields[i]
if tf.name.name() == name {
return t.Field(i), true
}
if tf.embedded() {
hasEmbeds = true
}
}
}
if !hasEmbeds {
return
}
return t.FieldByNameFunc(func(s string) bool { return s == name })
}

整个调用链条是比较简单的:

1
(v Value) FieldByName -> (t *rtype) FieldByName -> (t *structType) FieldByName
(t *structType) FieldByNameName

4 如何提高性能

4.1 避免使用反射

jsonMarshalUnmarshal

4.2 缓存

FieldByNameFieldFieldByNameNameIndex

我们利用缓存,优化下刚才的测试用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func BenchmarkReflect_FieldByNameCacheSet(b *testing.B) {
typ := reflect.TypeOf(Config{})
cache := make(map[string]int)
for i := 0; i < typ.NumField(); i++ {
cache[typ.Field(i).Name] = i
}
ins := reflect.New(typ).Elem()
b.ResetTimer()
for i := 0; i < b.N; i++ {
ins.Field(cache["Name"]).SetString("name")
ins.Field(cache["IP"]).SetString("ip")
ins.Field(cache["URL"]).SetString("url")
ins.Field(cache["Timeout"]).SetString("timeout")
}
}

测试结果如下:

1
2
3
4
5
6
7
8
9
10
$ go test -bench="Set$" . -v
goos: darwin
goarch: amd64
pkg: example/hpg-reflect
BenchmarkSet-8 1000000000 0.303 ns/op
BenchmarkReflect_FieldSet-8 33429990 34.1 ns/op
BenchmarkReflect_FieldByNameSet-8 3612130 331 ns/op
BenchmarkReflect_FieldByNameCacheSet-8 14575906 78.2 ns/op
PASS
ok example/hpg-reflect 4.280s

消耗时间从原来的 10 倍,缩小到了 2 倍。

附 推荐与参考


edit this page last updated at 2023-04-28