天天看點

Java 反射機制,速度提高 1000 倍

想讓代碼運作快1000倍,同時不改變複雜度,正如标題所說的,使用Java反射機制,可以讓代碼運作得更快。

首先來解釋一下為什麼會首先使用反射機制。

有一個接口(表示一個樹節點)和一個實作這個接口的大量類(100+)。訣竅在于,樹是異構的,每個節點類型可以有不同數量的子節點,或者以不同的方式存儲它們。

我需要讓代碼能夠在這樣的組合樹上運作起來。簡單的方法是簡單地向接口添加一個children()方法,并在每個節點中實作它。當然,這很繁瑣,也很乏味。

相反,我注意到所有的子節點都是直接的字段,或者聚集在包含節點集合的字段中。是以可以用反射的方式寫一小段代碼,這也對每一個節點都适用!

我已經在Github上放了一個簡化版的代碼。我會把相關的部分聯系起來。

這是我提出的第一版本代碼:WalkerDemoSlowest.java。

它相當簡單:擷取節點類的方法,過濾掉那些不是getter的方法,然後隻考慮傳回節點或節點集合。調用這些方法,并在子節點上遞歸地調用walk方法。

如果我說這樣的進展很慢,有人會感到驚訝嗎?

有一個簡單的調整,可以使它更快:使用緩存方法查找。

下面是緩存版本:WalkerDemoSlow.java

這和每個實作節點的類都是一樣的,建立一個ClassData對象來緩存所有相關的getter方法,是以隻需要查找一次,這會産生一個令人滿意的10倍加速。

不幸的是,這仍然太慢了。是以我向谷歌尋求幫助,發現了一個很有用的StackOverflow社群。

有答案建議使用LambdaMetafactory,這是一個标準的庫類,它支援lambda文法調用。

細節在我看來有些模糊,但似乎通過使用這些工具,可以在代碼中“打開編譯器”,并優化反射機制來進行本機調用。這就是一種假設。

這是代碼:walkerdemofast.java

現在,我的代碼可以做到100倍的加速。然而,在寫這篇文章的時候,想用一些代碼片段來示範這個效果,但是沒有成功。我試着給接口實作3個子類,并使用一些僞方法進行過濾,但還是沒有效果。第二版和第三版的代碼運作速度差不多。

我重新檢查了原來的代碼,一切看起來都很好。在原始代碼中,樹是通過解析一些源檔案得到的抽象文法樹(AST)。如果限制了前14個源檔案的輸入,我發現會得到不同的結果。

這些檔案相對較短(幾乎沒有10行),文法簡單。但僅僅有這些,第二和第三版代碼仍會以同樣的速度運作。但是在第15個檔案中進行輸入(少于100行),那麼第二個版本的代碼會花費36秒,而第三個版本代碼會在0.2秒内完成,這是700倍的差異。

我的假設是,如果場景足夠簡單,優化器會注意到正在運作的代碼并選擇離開。在更複雜的情況下,它會耗盡優化預算,然後回到未優化的版本以及糟糕的性能狀态。但是,優化器已經足夠靈活,如果有一個能擊敗它的示例,那似乎是非常成功的。

我有點好奇LambdaMetafactory會有什麼樣的可能性。在我的示例中,它會産生奇迹,因為反射調用比簡單的緩存查找要昂貴得多。但它是否也能對正常代碼進行優化處理呢?這似乎不太可能讓megamorphic call sites提供幫助,因為編譯的方法必須以某種方式檢索,而查找的成本将使收益相形見绌。

但是,如何在運作組合代碼時進行優化呢?可以提供資料結構,或者為資料結構提供解釋器,并使用LambdaMetafactory“編譯”它們。這是否足夠智能呢,可以對給定資料結構的代碼進行部分評估,進而将解釋器轉換成等價的“plain”代碼?

順便說一下,這正是Truffle架構所采用的方法,它在Graal VM上運作,是以這個想法肯定有一定的意義。可能暫時無法使用目前的JVM,是以需要修改GraalVM。

在任何情況下,都會盡量使一些功能成為一個庫,可以在“正常程式”(非編譯器)中使用。編寫簡單的解釋器通常是解決一些問題的最簡單方法。

文章出自http://geek.csdn.net/news/detail/236973

原文連結: