4

揭秘反射真的很耗时吗,射10万次用时多久

 2 years ago
source link: http://www.androidchina.net/12559.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

揭秘反射真的很耗时吗,射10万次用时多久 – Android开发中文站

最新消息:欢迎访问Android开发中文站!商务联系微信:loading_in
你的位置:Android开发中文站 > 热点资讯 > 揭秘反射真的很耗时吗,射10万次用时多久
揭秘反射真的很耗时吗,射10万次用时多久

揭秘反射真的很耗时吗,射10万次用时多久

hi 大家好,我是 DHL。公众号:ByteCode ,专注分享有趣硬核原创内容,Kotlin、Jetpack、性能优化、系统源码、算法及数据结构、动画、大厂面经。

全文分为 视频版文字版

  • 文字版: 侧重于细节上的知识点更多、更加详细
  • 视频版: 通过动画展示讲解,更加的清楚、直观

视频版本地址:https://b23.tv/Hprua24

无论是在面试过程中,还是看网络上各种技术文章,只要提到反射,不可避免都会提到一个问题,反射会影响性能吗?影响有多大?如果在写业务代码的时候,你用到了反射,都会被 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-reflect:$kotlin_version"

在开始分析,我们需要对比 Java 了解一下 Kotlin 反射基本语法。

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

// 获取 KClass
Person().javaClass.kotlin
Person::class
Class.forName("com.hi-dhl.demo.Person").kotlin
  • kotlin 的 KProperty 对应 Java 的 Field,Java 的 Fieldgetter/setter 方法,但是在 Kotlin 中没有 Field,分为了 KPropertyKMutableProperty,当变量用 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, 其子类型分别是 MethodFieldConstructor
// Java
Person().javaClass.getConstructor().newInstance() // 构造方法
Person().javaClass.getDeclaredMethod("getName") // 成员方法

// Kotlin
Person::class.primaryConstructor?.call() // 构造方法
Person::class.declaredFunctions.find { it.name == "getName" }  // 成员方法

无论是使用 Java 还是 Kotlin 最终测试出来的结论都是一样的,了解完基本反射语法之后,我们分别测试上述四种场景反射前后的耗时。

正常创建对象

@Benchmark
fun createInstance(bh: Blackhole) {
    for (index in 0 until 100_000) {
        bh.consume(Person())
    }
}

五轮测试平均耗时 0.578 ms/op 。需要重点注意,这里使用了 JMH 提供 Blackhole,通过 Blackholeconsume() 方法来避免 JIT 带来的优化, 让结果更加接近真实。

在对象创建过程中,会先检查类是否已经加载,如果类已经加载了,会直接为对象分配空间,其中最耗时的阶段其实是类的加载过程(加载->验证->准备->解析->初始化)。

通过反射创建对象

@Benchmark
fun createReflectInstance(bh: Blackhole) {
    for (index in 0 until 100_000) {
        bh.consume(Person::class.primaryConstructor?.call())
    }
}

五轮测试平均耗时 4.710 ms/op,是正常创建对象的 9.4 倍,这个结果是很惊人,如果将中间操作(获取构造方法)从循环中提取出来,那么结果会怎么样呢。

反射优化

@Benchmark
fun 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 = true

isAccessible 是用来判断是否需要进行安全检査,设置为 true 表示关掉安全检查,将会减少安全检査产生的耗时,五轮测试平均耗时 0.943 ms/op,反射速度进一步提升了。

几轮测试最后的结果如下图示。

1ee5891f36b441cf9b22b38e94d598e7~tplv-k3u1fbpfcp-zoom-in-crop-mark:1304:0:0:0.awebp

正常调用

@Benchmark
fun callMethod(bh: Blackhole) {
    val person = Person()
    for (index in 0 until 100_000) {
        bh.consume(person.getName())
    }
}

五轮测试平均耗时 0.422 ms/op

反射调用

@Benchmark
fun 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 代码)从循环中提取出来,结果会怎么样呢。

反射优化

@Benchmark
fun 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,反射速度进一步提升了。

几轮测试最后的结果如下图示。

37c633077010427d8a0827ca794d6767~tplv-k3u1fbpfcp-zoom-in-crop-mark:1304:0:0:0.awebp

正常调用

@Benchmark
fun callPropertie(bh: Blackhole) {
    val person = Person()
    for (index in 0 until 100_000) {
        bh.consume(person.age)
    }
}

五轮测试平均耗时 0.241 ms/op

反射调用

@Benchmark
fun 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 倍,然后我们将中间操作(获取属性的代码)从循环中提出来。

反射优化

@Benchmark
fun 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,反射速度进一步提升了。

几轮测试最后的结果如下图示。

b6eedcd1d0d647f0bde873b56c697935~tplv-k3u1fbpfcp-zoom-in-crop-mark:1304:0:0:0.awebp

正常调用

@Benchmark
fun callCompaion(bh: Blackhole) {
    for (index in 0 until 100_000) {
        bh.consume(Person.getAddress())
    }
}

五轮测试平均耗时 0.470 ms/op

反射调用

@Benchmark
fun 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 代码)从循环中提出来的结果。

反射优化

@Benchmark
fun 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,反射速度进一步提升了。

几轮测试最后的结果如下图所示。

e729f6f992e34acda0be24dd0a1c4eaf~tplv-k3u1fbpfcp-zoom-in-crop-mark:1304:0:0:0.awebp

我们对比了四种常用的场景: 创建对象方法调用属性调用伴生对象。分别测试了反射前后的耗时,最后汇总一下五轮 10 万次测试平均值。

正常调用 反射 反射优化后 反射优化后关掉安全检查
创建对象 0.578 ms/op 4.710 ms/op 1.018 ms/op 0.943 ms/op
方法调用 0.422 ms/op 10.533 ms/op 0.844 ms/op 0.687 ms/op
属性调用 0.241 ms/op 12.432 ms/op 1.362 ms/op 1.202 ms/op
伴生对象 0.470 ms/op 5.661 ms/op 0.840 ms/op 0.702 ms/op

每个场景反射前后的耗时如下图所示。

f333af3964f543e6b73bdb929e6b1c7c~tplv-k3u1fbpfcp-zoom-in-crop-mark:1304:0:0:0.awebp

在我们的印象中,反射就是恶魔,影响会非常大,但是从上面的表格看来,反射确实会有一定的影响,但是如果我们合理使用反射,优化后的反射结果并没有想象的那么大,这里有几个建议。

  • 在频繁的使用反射的场景中,将反射中间操作提取出来缓存好,下次在使用反射直接从缓存中取即可
  • 关掉安全检查,可以进一步提升性能

最后我们在看一下单次创建对象和单次反射创建对象的耗时,如下图所示。

bce70a34ce494e23a8158195352ac38e~tplv-k3u1fbpfcp-zoom-in-crop-mark:1304:0:0:0.awebp

Score 表示结果,Error 表示误差范围,在考虑误差的情况下,它们的耗时差距在 微妙别以内

当然根据设备的不同(高端机、低端机),还有系统、复杂的类等等因素,反射所产生的影响也是不同的。反射在实际项目中应用的非常的广泛,很多设计和开发都和反射有关,比如通过反射去调用字节码文件、调用系统隐藏 Api、动态代理的设计模式,Android 逆向、著名的 Spring 框架、各类 Hook 框架等等。

文章中的代码已经上传到 github 仓库 KtPractice: https://github.com/hi-dhl/KtPractice

ftd2

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK