您當前的位置:首頁 > 攝影

揭秘反射真的很耗時嗎,射 10 萬次用時多久

作者:由 程式設計師DHL 發表于 攝影時間:2022-05-13

全文分為

影片版

文字版

文字版:

側重於細節上的知識點更多、更加詳細

影片版:

透過動畫展示講解,更加的清楚、直觀

影片版本地址: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

,我們可以透過以下方式完成

KClass

Class

之間互相轉化

// 獲取 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 的

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

// 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

,透過

Blackhole

consume()

方法來避免 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

,反射速度進一步提升了。

幾輪測試最後的結果如下圖示。

揭秘反射真的很耗時嗎,射 10 萬次用時多久

方法呼叫

正常呼叫

@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

,反射速度進一步提升了。

幾輪測試最後的結果如下圖示。

揭秘反射真的很耗時嗎,射 10 萬次用時多久

屬性呼叫

正常呼叫

@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

,反射速度進一步提升了。

幾輪測試最後的結果如下圖示。

揭秘反射真的很耗時嗎,射 10 萬次用時多久

伴生物件

正常呼叫

@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

,反射速度進一步提升了。

幾輪測試最後的結果如下圖所示。

揭秘反射真的很耗時嗎,射 10 萬次用時多久

總結

我們對比了四種常用的場景:

建立物件

方法呼叫

屬性呼叫

伴生物件

。分別測試了反射前後的耗時,最後彙總一下五輪 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

每個場景反射前後的耗時如下圖所示。

揭秘反射真的很耗時嗎,射 10 萬次用時多久

在我們的印象中,反射就是惡魔,影響會非常大,但是從上面的表格看來,反射確實會有一定的影響,但是如果我們合理使用反射,最佳化後的反射結果並沒有想象的那麼大,這裡有幾個建議。

在頻繁的使用反射的場景中,將反射中間操作提取出來快取好,下次在使用反射直接從快取中取即可

關掉安全檢查,可以進一步提升效能

最後我們在看一下單次建立物件和單次反射建立物件的耗時,如下圖所示。

揭秘反射真的很耗時嗎,射 10 萬次用時多久

Score

表示結果,

Error

表示誤差範圍,在考慮誤差的情況下,它們的耗時差距在

微妙別以內

當然根據裝置的不同(高階機、低端機),還有系統、複雜的類等等因素,反射所產生的影響也是不同的。反射在實際專案中應用的非常的廣泛,很多設計和開發都和反射有關,比如透過反射去呼叫位元組碼檔案、呼叫系統隱藏 Api、動態代理的設計模式,Android 逆向、著名的 Spring 框架、各類 Hook 框架等等。

文章中的程式碼已經上傳到 github 倉庫 KtPractice: https://github。com/hi-dhl/KtPractice

全文到這裡就結束了,感謝你的閱讀,如果有幫助,歡迎 在看 、 點贊 、 收藏 、 分享 給身邊的朋友。

標簽: 反射  Ms  person  bh  耗時