Go基础—反射,性能和灵活性的双刃剑

这篇具有很好参考价值的文章主要介绍了Go基础—反射,性能和灵活性的双刃剑。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1 简介

现在的一些流行设计思想需要建立在反射基础上,如控制反转(Inversion Of Control,IOC)和依赖注入(Dependency Injection,DI)
Go 语言中非常有名的 Web 框架 martinihttps://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 服务器追求的目标。因此,反射在带来灵活性的同时,也带上了性能低下的桎梏。

要用好反射这把双刃剑,就需要详细了解反射的性能。下面的一些基准测试从多方面对比了原生调用和反射调用的区别。

2 结构体成员赋值对比

反射经常被使用在结构体上,因此结构体的成员访问性能就成为了关注的重点。下面例子中使用一个被实例化的结构体,访问它的成员,然后使用 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
    }
}

可以发现,整个设置过程都是指针转换及赋值,没有遍历及内存操作等相对耗时的算法。

3 结构体成员搜索并赋值对比

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() 方法比较严重的低效率问题。

4 调用函数对比

反射的函数调用,也是使用反射中容易忽视的性能点,下面展示对普通函数的调用过程。

// 一个普通函数
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类型返回。因此,反射调用函数的性能堪忧。

5 基准测试结果对比

测试结果如下:

$ 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 倍。

经过基准测试结果的数值分析及对比,最终得出以下结论:文章来源地址https://www.toymoban.com/news/detail-613360.html

  • 能使用原生代码时,尽量避免反射操作。
  • 提前缓冲反射值对象,对性能有很大的帮助。
  • 避免反射函数调用,实在需要调用时,先提前缓冲函数参数列表,并且尽量少地使用返回值。

到了这里,关于Go基础—反射,性能和灵活性的双刃剑的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【开源与项目实战:开源实战】87 | 开源实战五(上):MyBatis如何权衡易用性、性能和灵活性?

    上几节课我们讲到了 Spring 框架,剖析了背后蕴含的一些通用设计思想,以及用到的十几种设计模式。从今天开始,我们再剖析另外一个 Java 项目开发中经常用到的框架:MyBatis。因为内容比较多,同样,我们也分三节课来讲解。 第一节课,我们分析 MyBatis 如何权衡代码的易用

    2024年02月12日
    浏览(30)
  • NoSQL 数据库管理工具,搭载强大支持:Redis、Memcached、SSDB、LevelDB、RocksDB,为您的数据存储提供无与伦比的灵活性与性能!

    【官网地址】:http://www.redisant.cn/nosql 直观的用户界面 从单一应用程序中同时连接 Redis、Memcached、SSDB、LevelDB、RocksDB,你可以快速轻松地创建、管理和维护数据库。 简洁的数据操作 快速搜索、编辑、删除、创建键;支持丰富的数据类型,包括:JSON、XML、HEX、MsgPack、YAML、整

    2024年02月21日
    浏览(32)
  • 基线核查与系统加固:构筑坚实基础防御的双刃剑

     引言 网络安全在当今信息社会扮演着不可或缺的角色,而安全基线核查和系统加固作为网络安全防御的基础工作至关重要。本文将深入探讨安全基线核查和系统加固的定义、必要性,以及它们在安全管理中的重要要求。旨在强调这些基础工作在构建健壮网络安全体系中的关

    2024年04月14日
    浏览(29)
  • 开源与远程工作:灵活性与协作

    🌷🍁 博主猫头虎 带您 Go to New World.✨🍁 🦄 博客首页——猫头虎的博客🎐 🐳《面试题大全专栏》 文章图文并茂🦕生动形象🦖简单易学!欢迎大家来踩踩~🌺 🌊 《IDEA开发秘籍专栏》学会IDEA常用操作,工作效率翻倍~💐 🌊 《100天精通Golang(基础入门篇)》学会Golang语言

    2024年02月10日
    浏览(26)
  • 趣味算法——链表:灵活性与高效性的完美结合

    链表(Linked List)是一种常见的基础数据结构,它通过“链接”的方式来存储数据,相当于是把数据分散存放在内存中,每一部分数据由一个存储元素和一个指针组成,其中,存储元素用于保存或者表示数据,指针则用来标记下一个存储元素的地址,这样,将分散的数据链接

    2024年02月09日
    浏览(29)
  • CSS Position与Float:探索布局的灵活性

    在网页设计中,我们常常需要对元素进行布局,并使其相互排列或定位。CSS提供了多种方式来实现这些目标,其中包括 position 和 float 属性。本文将深入讲解这两个属性以及它们在布局中的应用。 相对定位(Relative) 相对定位通过设置 position: relative; 属性来移动元素相对于其

    2024年02月10日
    浏览(28)
  • Sentinel 新版本发布,提升配置灵活性以及可观测配套

    作者:屿山 Sentinel 是阿里巴巴集团开源的,面向分布式、多语言异构化服务架构的流量治理组件,承接了阿里巴巴近 15 年的双十一大促流量的核心场景,例如秒杀、冷启动、消息削峰填谷、集群流量控制、实时熔断下游不可用服务等,是保障微服务高可用的利器。开源以来

    2024年01月24日
    浏览(29)
  • 技术挑战:AI模型的可扩展性与灵活性

    在过去的几年里,人工智能(AI)已经成为了我们生活中不可或缺的一部分。从自动驾驶汽车到语音助手,AI技术的发展和应用不断地拓展。然而,随着AI技术的不断发展,我们面临着新的挑战:如何让AI模型具有更高的可扩展性和灵活性。 在本文中,我们将探讨AI模型的可扩展性

    2024年02月21日
    浏览(30)
  • C++ 多级继承与多重继承:代码组织与灵活性的平衡

    多级继承是一种面向对象编程(OOP)特性,允许一个类从多个基类继承属性和方法。它使代码更易于组织和维护,并促进代码重用。 在 C++ 中,使用 : 符号来指定继承关系。多级继承的语法如下: 在这个例子中, DerivedClass 从 BaseClass1 和 BaseClass2 继承。这意味着它将继承这两

    2024年04月25日
    浏览(24)
  • Animation Rigging 如何让你的Avatar人物更具灵活性

    Animation Rigging 是 Unity 官方发布的可以对 Avatar 人物骨骼进行约束的工具,已经有稳定的经过验证的 Vertified 包体,可以将其理解为一个 IK 工具,使用它可以让我们的人物动作表现更具灵活性。 Rig Builder 依赖 Animator 组件,所以将其与 Avatar 的 Animator 组件挂载于同一个物体上,

    2023年04月21日
    浏览(59)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包