我可以优化这个Go反射函数,使其不那么慢吗?

huangapple go评论114阅读模式
英文:

Can I optimize this Go reflect function so it isn't as slow?

问题

我正在进行一个项目,需要为所有支持的方法构建部分更新。每个部分更新都需要一个不同的结构体,具有不同的字段和字段数量,并且不知道哪些字段会存在或不存在。我决定遍历每个结构体字段,如果存在则将其添加到一个数组中,在最后返回该数组。我还花了一些时间对几个看起来最符合实际的函数进行了基准测试,以帮助做出决策。

所有结构体字段都将是指针。在这个前提下,这是我编写的函数。

注意:我无法为此创建一个playground示例,因为它不支持基准测试。我将链接完整的类,并将解释放在它们上面。

1)为每个部分更新结构体创建一个映射函数,我将单独检查每个字段。如果字段不为nil,我将以[key,value]的格式将值放入一个二维数组中。在处理完结构体后,返回该数组。
2)创建一个使用泛型和反射的单个映射函数,执行与上述相同的操作。

这是我运行的基准测试方法:

  1. // main_test.go
  2. package main
  3. import (
  4. "testing"
  5. "time"
  6. )
  7. func BenchmarkBoth(b *testing.B) {
  8. field1 := "testfield1"
  9. field2 := "testfield2"
  10. field3 := "testfield3"
  11. field4 := "testfield4"
  12. field5 := "testfield5"
  13. date1, _ := time.Parse(time.RFC3339, "2004-10-16T12:40:53.00Z")
  14. str := &updateRequest{
  15. FieldOne: &field1,
  16. FieldTwo: &field2,
  17. FieldThree: &field3,
  18. FieldFour: &field4,
  19. FieldFive: &field5,
  20. FieldSix: &date1,
  21. }
  22. b.Run("ManualUpdateRequestMapping", func(b *testing.B) {
  23. for i := 0; i < b.N; i++ {
  24. _ = ManualUpdateRequestMapping(str)
  25. }
  26. })
  27. b.Run("ReflectUpdateRequestMapping", func(b *testing.B) {
  28. for i := 0; i < b.N; i++ {
  29. _ = ReflectUpdateRequestMapping(str)
  30. }
  31. })
  32. }

下面是测试结果和使用的CPU:

  1. cpu: 12th Gen Intel(R) Core(TM) i9-12900KF
  2. BenchmarkBoth/ManualUpdateRequestMapping-24 3560083 331.9 ns/op 368 B/op 8 allocs/op
  3. BenchmarkBoth/ReflectUpdateRequestMapping-24 1393377 866.7 ns/op 648 B/op 21 allocs/op
  4. PASS
  5. ok com.example.stack 3.745s

我预计反射函数会慢一些,但不会慢到大约2.5倍。它似乎还分配了大约2.5倍的资源。我在上面的代码中搞砸了吗,还是反射真的慢那么多?

如果有任何建议可以使上面的代码更高效,我愿意接受所有建议。我已经使用Go语言工作了大约3个月,所以如果我在上面的代码中犯了任何错误,请轻一点。:)

英文:

I am working on a project where I need to build out the partial updates for all of the methods that will be supported. Each partial update will require a different struct, with different fields and number of fields, and not knowing which ones will be present or not. I decided on going over each struct field, and if it's present adding it to an array to return at the end. I also took some time to Benchmark a couple of functions that seemed most realistic to approach this problem, and to help make a decision.

All of the structs fields will be pointers. With that in mind, these are the functions I wrote.

Note: I can't create a playground example for this, because it doesn't support Benchmarks. I'll link the full classes, and put the explanation above them.

  1. Create a mapping function for each partial update struct, where I would check each field separately. If the field is not nil, I will put the value in a 2D array storing in a [key,value] format. After the struct has been processed, return the array.
  2. Create a single mapping function that uses Generics and Reflection to do the same as above.
  1. // main.go
  2. package main
  3. import (
  4. &quot;reflect&quot;
  5. &quot;strings&quot;
  6. &quot;time&quot;
  7. )
  8. type updateRequest struct {
  9. FieldOne *string `json:&quot;field_one,omitempty&quot;`
  10. FieldTwo *string `json:&quot;field_two,omitempty&quot;`
  11. FieldThree *string `json:&quot;field_three,omitempty&quot;`
  12. FieldFour *string `json:&quot;field_four,omitempty&quot;`
  13. FieldFive *string `json:&quot;field_five,omitempty&quot;`
  14. FieldSix *time.Time `json:&quot;field_six,omitempty&quot; time_format:&quot;2006-01-02&quot;`
  15. }
  16. // Mapping function that would need to be recreated for each partial update struct.
  17. func ManualUpdateRequestMapping(req *updateRequest) [][]string {
  18. vals := make([][]string, 0, 6)
  19. if req.FieldOne != nil {
  20. vals = append(vals, []string{&quot;field_one&quot;, *req.FieldOne})
  21. }
  22. if req.FieldTwo != nil &amp;&amp; req.FieldThree != nil {
  23. vals = append(vals, []string{&quot;field_two&quot;, *req.FieldTwo}, []string{&quot;field_three&quot;, *req.FieldThree})
  24. }
  25. if req.FieldFour != nil {
  26. vals = append(vals, []string{&quot;field_four&quot;, *req.FieldFour})
  27. }
  28. if req.FieldFive != nil {
  29. vals = append(vals, []string{&quot;field_five&quot;, *req.FieldFive})
  30. }
  31. if req.FieldSix != nil {
  32. vals = append(vals, []string{&quot;field_six&quot;, req.FieldSix.Format(time.RFC3339)})
  33. }
  34. return vals
  35. }
  36. // Generics and Reflection function
  37. func ReflectUpdateRequestMapping[T *updateRequest](str T) [][]string {
  38. valHolder := reflect.ValueOf(*str)
  39. if valHolder.Kind() != reflect.Struct {
  40. return nil
  41. }
  42. vals := make([][]string, 0, valHolder.NumField())
  43. for i := 0; i &lt; valHolder.NumField(); i++ {
  44. if valHolder.Field(i).IsNil() {
  45. continue
  46. }
  47. spl := strings.Split(valHolder.Type().Field(i).Tag.Get(&quot;json&quot;), &quot;,&quot;)
  48. if valHolder.Field(i).Elem().Type() != reflect.TypeOf(time.Time{}) {
  49. vals = append(vals, []string{spl[0], valHolder.Field(i).Elem().String()})
  50. } else {
  51. vals = append(vals, []string{spl[0], valHolder.Field(i).Interface().(*time.Time).Format(time.RFC3339)})
  52. }
  53. }
  54. return vals
  55. }

This is the benchmark method I ran with:

  1. // main_test.go
  2. package main
  3. import (
  4. &quot;testing&quot;
  5. &quot;time&quot;
  6. )
  7. func BenchmarkBoth(b *testing.B) {
  8. field1 := &quot;testfield1&quot;
  9. field2 := &quot;testfield2&quot;
  10. field3 := &quot;testfield3&quot;
  11. field4 := &quot;testfield4&quot;
  12. field5 := &quot;testfield5&quot;
  13. date1, _ := time.Parse(time.RFC3339, &quot;2004-10-16T12:40:53.00Z&quot;)
  14. str := &amp;updateRequest{
  15. FieldOne: &amp;field1,
  16. FieldTwo: &amp;field2,
  17. FieldThree: &amp;field3,
  18. FieldFour: &amp;field4,
  19. FieldFive: &amp;field5,
  20. FieldSix: &amp;date1,
  21. }
  22. b.Run(&quot;ManualUpdateRequestMapping&quot;, func(b *testing.B) {
  23. for i := 0; i &lt; b.N; i++ {
  24. _ = ManualUpdateRequestMapping(str)
  25. }
  26. })
  27. b.Run(&quot;ReflectUpdateRequestMapping&quot;, func(b *testing.B) {
  28. for i := 0; i &lt; b.N; i++ {
  29. _ = ReflectUpdateRequestMapping(str)
  30. }
  31. })
  32. }

Below is the CPU used and the results that come from the test:

  1. cpu: 12th Gen Intel(R) Core(TM) i9-12900KF
  2. BenchmarkBoth/ManualUpdateRequestMapping-24 3560083 331.9 ns/op 368 B/op 8 allocs/op
  3. BenchmarkBoth/ReflectUpdateRequestMapping-24 1393377 866.7 ns/op 648 B/op 21 allocs/op
  4. PASS
  5. ok com.example.stack 3.745s

I expected the Reflection function to be slower, but not ~2.5x slower. It also seems to allocate ~2.5x more resources per iteration. Did I botch something in the code above, or is Reflection just that much slower?

If there are any recommendations to make the code above more efficient, I am open to all suggestions. I've been working with Go for about 3 months now, so please go easy on me if I committed any Golang treason in the code above. 我可以优化这个Go反射函数,使其不那么慢吗?

答案1

得分: 1

反射比非反射代码慢。这里有一个改进方法。一些注意事项:

  • 通过将字段作为常规类型值获取并从那里进行操作,减少反射调用。
  • 不需要那种新潮的类型参数东西。
  • 说到Golang的背叛,语言的名称是Go。

解决了这些问题,下面是代码:

  1. func UpdateRequestMapping(p any) [][]string {
  2. v := reflect.ValueOf(p).Elem()
  3. if v.Kind() != reflect.Struct {
  4. return nil
  5. }
  6. t := v.Type()
  7. result := make([][]string, t.NumField())
  8. for i := 0; i < t.NumField(); i++ {
  9. var ok bool
  10. var value string
  11. switch f := v.Field(i).Interface().(type) {
  12. case *string:
  13. if f != nil {
  14. ok = true
  15. value = *f
  16. }
  17. case *time.Time:
  18. if f != nil {
  19. ok = true
  20. value = (*f).Format(time.RFC3339)
  21. }
  22. }
  23. if ok {
  24. name, _, _ := strings.Cut(t.Field(i).Tag.Get("json"), ",")
  25. result[i] = []string{name, value}
  26. }
  27. }
  28. return result
  29. }
英文:

Reflection is slower than non-reflection code. Here's an improvement. Some notes:

  • Reduce reflect calls by getting the field as a regular typed value and working from there.
  • There's no need for that new-fangled type parameter stuff.
  • Speaking of Golang treason, the name of the language is Go.

With that out of the way, here's the code:

  1. func UpdateRequestMapping(p any) [][]string {
  2. v := reflect.ValueOf(p).Elem()
  3. if v.Kind() != reflect.Struct {
  4. return nil
  5. }
  6. t := v.Type()
  7. result := make([][]string, t.NumField())
  8. for i := 0; i &lt; t.NumField(); i++ {
  9. var ok bool
  10. var value string
  11. switch f := v.Field(i).Interface().(type) {
  12. case *string:
  13. if f != nil {
  14. ok = true
  15. value = *f
  16. }
  17. case *time.Time:
  18. if f != nil {
  19. ok = true
  20. value = (*f).Format(time.RFC3339)
  21. }
  22. }
  23. if ok {
  24. name, _, _ := strings.Cut(t.Field(i).Tag.Get(&quot;json&quot;), &quot;,&quot;)
  25. result[i] = []string{name, value}
  26. }
  27. }
  28. return result
  29. }

huangapple
  • 本文由 发表于 2022年10月22日 10:25:15
  • 转载请务必保留本文链接:https://go.coder-hub.com/74160682.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定