广告
返回顶部
首页 > 资讯 > 后端开发 > GO >详解如何让Go语言中的反射加快
  • 871
分享到

详解如何让Go语言中的反射加快

2024-04-02 19:04:59 871人浏览 八月长安
摘要

目录切入点案例反射基本版优化一:加入缓存策略优化二:利用字段偏移量优化三:更改缓存 key 类型优化四:引入描述符总结最近读到一篇关于 Go 反射的文章,作者通过反射给结构体填充字段

最近读到一篇关于 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

优化三:更改缓存 key 类型

还能让它走得更快吗?如果我们对 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语言 反射内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

您可能感兴趣的文档:

--结束END--

本文标题: 详解如何让Go语言中的反射加快

本文链接: https://www.lsjlt.com/news/121119.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • 详解如何让Go语言中的反射加快
    目录切入点案例反射基本版优化一:加入缓存策略优化二:利用字段偏移量优化三:更改缓存 key 类型优化四:引入描述符总结最近读到一篇关于 Go 反射的文章,作者通过反射给结构体填充字段...
    99+
    2022-11-11
  • Go语言的反射机制详解
    反射是语言里面是非常重要的一个特性,我们经常会看见这个词,但是对于反射没有一个很好的理解,主要是因为对于反射的使用场景不太熟悉。 一、理解变量的内在机制 1.类型信息,元信息,是预先...
    99+
    2022-11-13
  • Go语言学习之反射的用法详解
    目录1. reflect 包1.1 获取变量类型1.2 断言处理类型转换2. ValueOf2.1 获取变量值2.2 类型转换3. Value.Set3.1 设置变量值3.2 示例4...
    99+
    2022-11-13
  • Go语言如何让Git加载更快更实时?
    Git是一款开源的分布式版本控制系统,广泛应用于软件开发和项目管理中。在使用Git的过程中,我们经常会遇到Git加载慢、卡顿等问题,这些问题可能会影响我们的工作效率。而Go语言,作为一种高效的编程语言,可以帮助我们解决这些问题。 本文将介...
    99+
    2023-09-04
    load 实时 git
  • Go语言学习教程之反射的示例详解
    目录介绍反射的规律1. 从接口值到反射对象的反射2. 从反射对象到接口值的反射3. 要修改反射对象,该值一定是可设置的介绍 reflect包实现运行时反射,允许一个程序操作任何类型...
    99+
    2022-11-11
  • Go语言中如何快速加载容器?
    Go语言作为一种高效、可靠的编程语言,近年来在容器化技术领域得到了广泛的应用。作为容器化技术的核心技术之一,容器的快速启动和加载一直是一个备受关注的问题。本文将介绍在Go语言中如何快速加载容器。 一、引言 在容器化技术中,容器的启动和加载是...
    99+
    2023-09-18
    关键字 load 容器
  • 如何在go语言中利用反射精简代码
    这篇文章主要为大家分析了如何在go语言中利用反射精简代码的相关知识点,内容详细易懂,操作细节合理,具有一定参考价值。如果感兴趣的话,不妨跟着跟随小编一起来看看,下面跟着小编一起深入学习“如何在go语言中利用反射精简代码”的知识吧。反射是 G...
    99+
    2023-06-05
  • 如何在Go语言中快速加载数据?
    Go语言是一门极具竞争力的编程语言,它在性能和开发效率方面都有很好的表现。在处理大量数据时,Go语言同样也有着出色的表现。在本文中,我们将介绍如何在Go语言中快速加载数据。 一、使用bufio包 Go语言的bufio包提供了一种快速读取大文...
    99+
    2023-11-15
    二维码 面试 load
  • Go语言中容器的加载过程详解
    在Go语言中,容器是非常重要的概念。容器的概念来源于操作系统,它可以帮助我们管理多个进程和线程,并且提供了一个隔离的环境,使得不同的进程和线程之间不会互相干扰。在Go语言中,我们可以通过使用goroutine和channel来实现容器的管...
    99+
    2023-09-18
    关键字 load 容器
  • 详解Go语言中数组,切片和映射的使用
    目录1.Arrays (数组)2.切片2.1 make创建切片3.映射MapArrays (数组), Slices (切片) 和 Maps (映射) 是常见的一类数据结构 1.Arr...
    99+
    2022-11-13
  • 详解go语言中sort如何排序
    目录sort 包源码解读前言如何使用基本数据类型切片的排序自定义 Less 排序比较器自定义数据结构的排序分析下源码不稳定排序稳定排序查找Interface总结参考sort 包源码解...
    99+
    2022-11-13
  • Go语言中如何实现容器文件的快速加载?
    随着云计算和容器技术的广泛应用,容器文件的快速加载已经成为了一个非常重要的需求。在Go语言中,我们可以通过一些优秀的库和技巧来实现这个目标。在本文中,我们将介绍如何使用Go语言来实现容器文件的快速加载,并且演示一些代码来帮助您更好地理解。...
    99+
    2023-09-08
    load 容器 文件
  • 如何在Django中添加HTTP API,让Go语言变得更有用?
    Django是一个非常流行的Python Web框架,它提供了很多方便的功能,使得构建Web应用变得更加简单。Go语言是一种快速、可靠、并发性强的编程语言,它可以用于构建高性能的Web应用程序。本文将介绍如何在Django中添加HTTP A...
    99+
    2023-07-23
    django api http
  • 详解Go语言如何实现字符串切片反转函数
    目录Python 中的 reverse 函数实现一个 reverse 反转函数利用两个切片实现前后两两原地交换反转为原切片的副本总结Python 中的 reverse 函数 Go 语...
    99+
    2022-11-11
  • Go语言编程算法,如何让你的代码更加高效?
    随着软件开发的不断发展,算法已经成为了计算机科学中的重要领域之一。而在Go语言编程中,优化算法也成为了一项重要的任务。本文将介绍一些可以帮助你让Go语言编程中的算法更加高效的技巧和实践。 一、使用切片代替数组 Go语言中的数组在编译时就已...
    99+
    2023-10-14
    异步编程 编程算法 打包
  • 如何让GO语言中的对象数组更灵活?
    Go语言中的对象数组是一种非常常见的数据类型,它可以存储相同类型的对象,并且可以对它们进行操作。然而,在实际开发过程中,我们有时需要更灵活的对象数组,以便更好地满足我们的需求。本文将介绍如何让Go语言中的对象数组更灵活。 一、使用切片 在G...
    99+
    2023-10-03
    对象 数组 编程算法
  • Go 语言中如何加载对象?一份详细教程
    在 Go 语言中,我们经常需要使用对象,但是如何加载这些对象呢?在本篇文章中,我们将会介绍 Go 语言中对象的加载过程,并且通过代码演示的方式让读者更好地理解。 Go 语言中对象的加载过程 在 Go 语言中,对象的加载过程通常包括三个...
    99+
    2023-08-22
    对象 load 教程
  • 了解Go语言如何加速Git加载的过程吗?
    Git是当今最流行的版本控制系统之一,被广泛应用于软件开发中。然而,Git在处理大型代码库时,会出现加载速度缓慢的问题。为了加速Git加载过程,许多开发人员开始使用Go语言编写自定义Git客户端。 在本篇文章中,我们将探讨如何使用Go语言...
    99+
    2023-09-04
    load 实时 git
  • Go语言中如何解析和加载容器文件?
    Go语言是一门高效、简单、易于学习的编程语言,其在容器技术方面有着广泛的应用。在容器技术中,文件是必不可少的组成部分,因此,Go语言中如何解析和加载容器文件成为了一个重要的话题。本文将介绍Go语言中如何解析和加载容器文件。 一、什么是容器文...
    99+
    2023-09-08
    load 容器 文件
  • Go语言中如何实现路由的反向代理
    Go语言中实现路由的反向代理引言:随着互联网的发展,网络请求转发和代理成为了开发中的常见需求。在Go语言中,反向代理是一种常用的技术,它可以将客户端的请求转发到不同的后端服务器。本文将介绍如何使用Go语言实现路由的反向代理。一、什么是反向代...
    99+
    2023-12-17
    Go语言 路由 反向代理
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作