反射真的很耗时吗?射10万次用时多久?

分享
藏宝库编辑 2024-9-14 08:16:23 31 0 来自 中国
作者:DHL
无论是在口试过程中,还是看网络上各种技能文章,只要提到反射,不可克制都会提到一个题目,反射会影响性能吗?影响有多大?假如在写业务代码的时间,你用到了反射,都会被 review 人发出魂魄拷问,为什么要用反射,有没有别的的办理办法。
而网上的答案都是如出一辙,好比反射慢、反射过程中频仍的创建对象占用更多内存、频仍的触发 GC 等等。那么反射慢多少?反射会占用多少内存?创建 1 个对象大概创建 10 万个对象耗时多少?单次反射大概 10 万次反射耗时多少?在我们的脑海中没有一个直观的概念,而本日这篇文章将会告诉你。
这篇文章,计划了几个常用的场景,一起讨论一下反射是否真的很耗时?末了会以图表的情势展示。
测试工具及方案

在开始之前我们须要界说一个反射类 Person。
class Person {    var age = 10    fun getName(): String {        return "I am DHL"    }    companion object {        fun getAddress(): String = "BJ"    }}针对上面的测试类,计划了以下几个常用的场景,验证反射前后的耗时。

  • 创建对象
  • 方法调用
  • 属性调用
  • 伴生对象
测试工具及代码:
JMH (Java Microbenchmark Harness),这是 Oracle 开辟的一个基准测试工具,他们比任何人都相识 JIT 以及 JVM 的优化对测试过程中的影响,以是使用这个工具可以尽大概的包管效果的可靠性。
基准测试是测试应用性能的一种方法,在特定条件下对某一对象的性能指标举行测试
本文的测试代码已经上传到 github 堆栈 KtPractice 接待前往检察。
github 堆栈 KtPractice:
https://github.com/hi-dhl/KtPractice
为什么使用 JMH
由于 JVM 会对代码做各种优化,假如只是在代码前后打印时间戳,如许盘算的效果是不置信的,由于忽略了 JVM 在实验过程中,对代码举行优化产生的影响。而 JMH 会尽大概的镌汰这些优化对最闭幕果的影响。
测试方案

  • 在单进程、单线程中,针对以上四个场景,每个场景测试五轮,每轮循环 10 万次,盘算它们的均匀值
  • 在实验之前,须要对代码举行预热,预热不会作为最闭幕果,预热的目标是为了构造一个相对稳固的情况,包管效果的可靠性。由于 JVM 会对实验频仍的代码,实验编译为呆板码,从而进步实验速率。而预热不但包罗编译为呆板码,还包罗 JVM 各种优化算法,只管镌汰 JVM 的优化,构造一个相对稳固的情况,低落对效果造成的影响。
  • JMH 提供 Blackhole,通过 Blackhole 的 consume 来克制 JIT 带来的优化
Kotlin 和 Java 的反射机制

本文测试代码全部使用 Kotlin,Koltin 是美满兼容 Java 的,以是同样也可以使用 Java 的反射机制,但是 Kotlin 自己也封装了一套反射机制,并不是用来代替 Java 的,是 Java 的加强版,由于 Kotlin 有自己的语法特点好比扩展方法伴生对象可空范例的查抄等等,假如想使用 Kotlin 反射机制,须要引入以下库。
implementation "org.jetbrains.kotlin:kotlin-reflectkotlin_version"在开始分析,我们须要对比 Java 相识一下 Kotlin 反射根本语法。

  • kotlin 的 KClass 对应 Java 的 Class,我们可以通过以下方式完成 KClass 和 Class 之间相互转化
// 获取 ClassPerson().javaClassPerson()::class.javaPerson::class.javaClass.forName("com.hi-dhl.demo.Person")// 获取 KClassPerson().javaClass.kotlinPerson::classClass.forName("com.hi-dhl.demo.Person").kotlin

  • kotlin 的 KProperty 对应 Java 的 Field,Java 的 Field 有 getter/setter 方法,但是在 Kotlin 中没有 Field,分为了 KProperty 和 KMutableProperty,当变量用 val 声明的时间,即属性为 KProperty,假如变量用 var 声明的时间,即属性为 KMutableProperty
// Java 的获取方式Person().javaClass.getDeclaredField("age")// Koltin 的获取方式Person::class.declaredMemberProperties.find { it.name == "age" }

  • 在 Kotlin 中 函数属性 以及 构造函数 的超范例都是 KCallable,对应的子范例是 KFunction (函数、构造方法等等) 和 KProperty / KMutableProperty (属性),而 Kotlin 中的 KCallable 对应 Java 的 AccessibleObject, 其子范例分别是 Method 、 Field 、 Constructor
// JavaPerson().javaClass.getConstructor().newInstance() // 构造方法Person().javaClass.getDeclaredMethod("getName") // 成员方法// KotlinPerson::class.primaryConstructor?.call() // 构造方法Person::class.declaredFunctions.find { it.name == "getName" }  // 成员方法无论是使用 Java 还是 Kotlin 终极测试出来的结论都是一样的,相识完根本反射语法之后,我们分别测试上述四种场景反射前后的耗时。
创建对象

正常创建对象
@Benchmarkfun createInstance(bh: Blackhole) {    for (index in 0 until 100_000) {        bh.consume(Person())    }}五轮测试均匀耗时 0.578 ms/op 。须要重点注意,这里使用了 JMH 提供 Blackhole,通过 Blackhole 的 consume() 方法来克制 JIT 带来的优化, 让效果更加靠近真实。
在对象创建过程中,会先查抄类是否已经加载,假如类已经加载了,会直接为对象分配空间,其中最耗时的阶段实在是类的加载过程(加载->验证->准备->分析->初始化)。
通过反射创建对象
@Benchmarkfun createReflectInstance(bh: Blackhole) {    for (index in 0 until 100_000) {        bh.consume(Person::class.primaryConstructor?.call())    }}五轮测试均匀耗时 4.710 ms/op,是正常创建对象的 9.4 倍,这个效果是很惊人,假如将中央操纵(获取构造方法)从循环中提取出来,那么效果会怎么样呢。
反射优化
@Benchmarkfun createReflectInstanceAccessibleTrue(bh: Blackhole) {    val constructor = Person::class.primaryConstructor    for (index in 0 until 100_000) {        bh.consume(constructor?.call())    }}正如你所见,我将中央操纵(获取构造方法)从循环中提取出来,五轮测试均匀耗时 1.018 ms/op,速率得到了很大的提升,相比反射优化前速率提升了 4.7 倍,但是假如我们在将安全查抄功能关掉呢。
constructor?.isAccessible = trueisAccessible 是用来判定是否须要举行安全检査,设置为 true 表现关掉安全查抄,将会镌汰安全检査产生的耗时,五轮测试均匀耗时 0.943 ms/op,反射速率进一步提升了。
几轮测试末了的效果如下图示。
1.png 方法调用

正常调用
@Benchmarkfun callMethod(bh: Blackhole) {    val person = Person()    for (index in 0 until 100_000) {        bh.consume(person.getName())    }}五轮测试均匀耗时 0.422 ms/op。
反射调用
@Benchmarkfun callReflectMethod(bh: Blackhole) {    val person = Person()    for (index in 0 until 100_000) {        val method = Person::class.declaredFunctions.find { it.name == "getName" }        bh.consume(method?.call(person))    }}五轮测试均匀耗时 10.533 ms/op,是正常调用的 26 倍。假如我们将中央操纵(获取 getName 代码)从循环中提取出来,效果会怎么样呢。
反射优化
@Benchmarkfun callReflectMethodAccessiblFalse(bh: Blackhole) {    val person = Person()    val method = Person::class.declaredFunctions.find { it.name == "getName" }    for (index in 0 until 100_000) {        bh.consume(method?.call(person))    }}将中央操纵(获取 getName 代码)从循环中提取出来了,五轮测试均匀耗时 0.844 ms/op,速率得到了很大的提升,相比反射优化前速率提升了 13 倍,假如在将安全查抄关掉呢。
method?.isAccessible = true五轮测试均匀耗时 0.687 ms/op,反射速率进一步提升了。
几轮测试末了的效果如下图示。
属性调用

正常调用
@Benchmarkfun callPropertie(bh: Blackhole) {    val person = Person()    for (index in 0 until 100_000) {        bh.consume(person.age)    }}五轮测试均匀耗时 0.241 ms/op 。
反射调用
@Benchmarkfun callReflectPropertie(bh: Blackhole) {    val person = Person()    for (index in 0 until 100_000) {        val propertie = Person::class.declaredMemberProperties.find { it.name == "age" }        bh.consume(propertie?.call(person))    }}五轮测试均匀耗时 12.432 ms/op,是正常调用的 62 倍,然后我们将中央操纵(获取属性的代码)从循环中提出来。
反射优化
@Benchmarkfun callReflectPropertieAccessibleFalse(bh: Blackhole) {    val person = Person::class.createInstance()    val propertie = Person::class.declaredMemberProperties.find { it.name == "age" }    for (index in 0 until 100_000) {        bh.consume(propertie?.call(person))    }}将中央操纵(获取属性的代码)从循环中提出来之后,五轮测试均匀耗时 1.362 ms/op,速率得到了很大的提升,相比反射优化前速率提升了 8 倍,我们在将安全查抄关掉,看一下效果。
propertie?.isAccessible = true五轮测试均匀耗时 1.202 ms/op,反射速率进一步提升了。
几轮测试末了的效果如下图示。
伴生对象

正常调用
@Benchmarkfun callCompaion(bh: Blackhole) {    for (index in 0 until 100_000) {        bh.consume(Person.getAddress())    }}五轮测试均匀耗时 0.470 ms/op 。
反射调用
@Benchmarkfun createReflectCompaion(bh: Blackhole) {    val classes = Person::class    val personInstance = classes.companionObjectInstance    val personObject = classes.companionObject    for (index in 0 until 100_000) {        val compaion = personObject?.declaredFunctions?.find { it.name == "getAddress" }        bh.consume(compaion?.call(personInstance))    }}五轮测试均匀耗时 5.661 ms/op,是正常调用的 11 倍,然后我们在看一下将中央操纵(获取 getAddress 代码)从循环中提出来的效果。
反射优化
@Benchmarkfun callReflectCompaionAccessibleFalse(bh: Blackhole) {    val classes = Person::class    val personInstance = classes.companionObjectInstance    val personObject = classes.companionObject    val compaion = personObject?.declaredFunctions?.find { it.name == "getAddress" }    for (index in 0 until 100_000) {        bh.consume(compaion?.call(personInstance))    }}将中央操纵(获取 getAddress 代码)从循环中提出来,五轮测试均匀耗时 0.840 ms/op,速率得到了很大的提升,相比反射优化前速率提升了 7 倍,现在我们在将安全查抄关掉。
compaion?.isAccessible = true五轮测试均匀耗时 0.702 ms/op,反射速率进一步提升了。
几轮测试末了的效果如下图所示。
4.png 总结

我们对比了四种常用的场景: 创建对象方法调用属性调用伴生对象。分别测试了反射前后的耗时,末了汇总一下五轮 10 万次测试均匀值。
正常调用反射反射优化后反射优化后关掉安全查抄创建对象0.578 ms/op4.710 ms/op1.018  ms/op0.943  ms/op方法调用0.422 ms/op10.533  ms/op0.844  ms/op0.687  ms/op属性调用0.241 ms/op12.432 ms/op1.362  ms/op1.202  ms/op伴生对象0.470 ms/op5.661 ms/op0.840  ms/op0.702  ms/op每个场景反射前后的耗时如下图所示。
5.png 在我们的印象中,反射就是恶魔,影响会非常大,但是从上面的表格看来,反射确实会有肯定的影响,但是假如我们公道使用反射,优化后的反射效果并没有想象的那么大,这里有几个发起。

  • 在频仍的使用反射的场景中,将反射中央操纵提取出来缓存好,下次在使用反射直接从缓存中取即可
  • 关掉安全查抄,可以进一步提升性能
末了我们在看一下单次创建对象和单次反射创建对象的耗时,如下图所示。
6.png Score 表现效果,Error 表现偏差范围,在思量偏差的情况下,它们的耗时差距在 玄妙别以内
固然根据装备的差别(高端机、低端机),尚有体系、复杂的类等等因素,反射所产生的影响也是差别的。反射在现实项目中应用的非常的广泛,许多计划和开辟都和反射有关,好比通过反射去调用字节码文件、调用体系隐蔽 Api、动态代理的计划模式,Android 逆向、闻名的 Spring 框架、各类 Hook 框架等等。
您需要登录后才可以回帖 登录 | 立即注册

Powered by CangBaoKu v1.0 小黑屋藏宝库It社区( 冀ICP备14008649号 )

GMT+8, 2024-10-19 06:21, Processed in 0.143222 second(s), 35 queries.© 2003-2025 cbk Team.

快速回复 返回顶部 返回列表