类固醇上的Java:5种超级有用的JIT优化技术
Java開發人員? 優化您的生產監控。 請在所有已記錄的錯誤,警告和異常之后查看源代碼,調用堆棧和變量狀態- 嘗試Takipi 。
最有用的JVM JIT優化有哪些?如何使用它們?
即使您沒有積極計劃,JVM也有很多技巧可以幫助您的代碼更好地執行。 有些人實際上不需要您提供任何幫助,其他人可以在此過程中使用一些幫助。
在這篇文章中,我們與Takipi的研發團隊負責人Moshe Tsur進行了聊天,并有機會分享了他有關JVM的即時(JIT)編譯器的一些技巧。
讓我們看看幕后發生了什么。
編寫一次,隨處運行,及時優化
大多數人都知道,Javac編譯器將其Java源代碼轉換為字節碼,然后由JVM運行,然后JVM將其編譯為Assembly,并將其提供給CPU。
很少有人知道兔子的洞越來越深。 JVM有2種不同的操作模式。
生成的字節碼是原始Java源代碼的準確表示,沒有進行任何優化。 當JVM將其轉換為Assembly時,事情變得更加棘手,魔術開始了:
兩者之間的鏈接是JIT編譯器。 您可能已經猜到,解釋模式比沒有中間人直接在CPU上運行要慢得多。
解釋模式為Java平臺提供了寶貴的機會來收集有關代碼及其在實踐中的實際行為的信息-使其有機會學習如何優化所生成的Assembly代碼。
經過足夠的運行時間后,JIT編譯器便能夠在程序集級別執行優化。 主要是,根據代碼的實際行為,最大程度地減少不必要的跳轉,從而增加應用程序的大量開銷。 此外,該過程不會在應用程序的“預熱階段”后停止,并且可能會多次發生以進一步優化組裝。
在Takipi ,我們正在構建一個Java代理來監視生產中的服務器,我們非常重視開銷。 每一點代碼都經過優化,在此過程中,我們有機會使用和學習了一些很酷的JIT功能。
以下是5個更有用的示例:
1.空檢查消除
在許多情況下,在開發中將某些條件檢查添加到代碼中非常有意義。 就像臭名昭著的null檢查一樣。 空值得到特殊處理也就不足為奇了,因為這是生產中發生最高例外的原因 。
但是,在大多數情況下,可以從優化的匯編代碼中消除顯式的空檢查,并且這種情況適用于很少發生的任何情況。 在這種情況確實發生的極少數情況下,JIT使用一種稱為“罕見陷阱”的機制,讓它知道需要回退并通過撤回在執行時需要執行的指令集來修復優化。發生–無需顯示實際的NullPointerException。
這些檢查之所以成為優化代碼的原因,是因為它們可能會在匯編代碼中引入跳轉,從而減慢其執行速度。
讓我們看一個簡單的例子:
private static void runSomeAlgorithm(Graph graph) {if (graph == null) {return;}// do something with graph }如果JIT看到從未使用null調用該圖,則編譯后的版本看起來就像它反映的代碼未經null檢查:
private static void runSomeAlgorithm(Graph graph) {// do something with graph }底線:我們不需要做任何特殊的事情就能享受到這種優化,并且當出現罕見情況時,JVM會知道“反優化”并獲得理想的結果。
2.分支預測
與Null Check Elimination(空檢查消除)相似,還有另一種JIT優化技術,可幫助它確定某些代碼行是否比其他代碼“ 更熱 ”并且發生頻率更高。
我們已經知道,如果某種條件很少或永遠不會成立,那么“罕見陷阱”機制很可能會介入并將其從已編譯的大會中消除。
如果存在不同的平衡,則IF條件的兩個分支都成立,但是一個分支的發生比另一個分支多,JIT編譯器可以根據最常見的一個對它們進行重新排序,并顯著減少Assembly跳轉的次數。
這是實際的工作方式:
private static int isOpt(int x, int y) {int veryHardCalculation = 0;if (x >= y) {veryHardCalculation = x * 1000 + y;} else {veryHardCalculation = y * 1000 + x;}return veryHardCalculation; }現在,假設大部分時間x <y,該條件將被翻轉以統計地減少Assembly跳轉的次數:
private static int isOpt(int x, int y) {int veryHardCalculation = 0;if (x < y) {// this would not require a jumpveryHardCalculation = y * 1000 + x;return veryHardCalculation;} else {veryHardCalculation = x * 1000 + y;return veryHardCalculation;} }如果您不確定,我們實際上已經對其進行了一些測試,并提取了經過優化的Assembly代碼:
0x00007fd715062d0c: cmp %edx,%esi 0x00007fd715062d0e: jge 0x00007fd715062d24 ;*if_icmplt; - Opt::isOpt@4 (line 117)如您所見,跳轉指令是jge(如果大于或等于,則跳轉),與條件的else分支相對應。
底線:我們享受的另一種即用型JIT優化。 在最近的文章中,我們還討論了分支預測,其中涉及一些最有趣的Stackoverflow Java答案 。
在此處顯示的示例中,我們看到了如何使用分支預測來解釋為什么在有助于分支預測的某些條件下在排序數組上運行操作要快得多的原因。
3.循環展開
您可能已經注意到,JIT編譯器不斷嘗試消除已編譯代碼中的Assembly跳轉。
這就是為什么循環聞起來像麻煩。
每次迭代實際上都是一個Assembly跳轉回指令集的開始。 通過循環展開,JIT編譯器將打開循環,并僅一個接一個地重復相應的Assembly指令。
從理論上講,當將Java轉換為字節碼時,javac可以做類似的事情,但是JIT編譯器對代碼將在其上運行的實際CPU有了更好的了解,并且知道如何以更加有效的方式微調代碼。
例如,讓我們看一下將矩陣乘以向量的方法:
private static double[] loopUnrolling(double[][] matrix1, double[] vector1) {double[] result = new double[vector1.length];for (int i = 0; i < matrix1.length; i++) {for (int j = 0; j < vector1.length; j++) {result[i] += matrix1[i][j] * vector1[j];}}return result; }展開的版本如下所示:
private static double[] loopUnrolling2(double[][] matrix1, double[] vector1) {double[] result = new double[vector1.length];for (int i = 0; i < matrix1.length; i++) {result[i] += matrix1[i][0] * vector1[0];result[i] += matrix1[i][1] * vector1[1];result[i] += matrix1[i][2] * vector1[2];// and maybe it will expand even further - e.g. 4 iterations, thus// adding code to fix the indexing// which we would waste more time doing correctly and efficiently}return result; }一次又一次地重復相同的操作,而不會產生跳轉開銷:
.... 0x00007fd715060743: vmovsd 0x10(%r8,%rcx,8),%xmm0 ;*daload; - Opt::loopUnrolling@26 (line 179) 0x00007fd71506074a: vmovsd 0x10(%rbp),%xmm1 ;*daload; - Opt::loopUnrolling@36 (line 179) 0x00007fd71506074f: vmulsd 0x10(%r12,%r9,8),%xmm1,%xmm1 0x00007fd715060756: vaddsd %xmm0,%xmm1,%xmm0 ;*dadd; - Opt::loopUnrolling@38 (line 179) 0x00007fd71506075a: vmovsd %xmm0,0x10(%r8,%rcx,8) ;*dastore; - Opt::loopUnrolling@39 (line 179) ....底線: JIT編譯器在展開循環方面做得很好,只要您使它們的內容簡單而沒有任何不必要的復雜性即可。
同時也不建議您嘗試優化此過程并自行展開,結果可能會導致更多問題,甚至無法解決。
4.內聯方法
接下來,進行另一個跳躍殺手優化。 實際上,方法調用是匯編跳轉的重要來源。 在可能的情況下,JIT編譯器將嘗試內聯它們,并消除往返的跳轉,發送參數和返回值的需要-將其全部內容傳遞給調用方法。
還可以通過2個JVM參數來微調JIT編譯器內聯方法的方式:
例如,讓我們看一下一種計算簡單線的坐標的方法:
private static void calcLine(int a, int b, int from, int to) {Line l = new Line(a, b);for (int x = from; x <= to; x++) {int y = l.getY(x);System.err.println("(" + x + ", " + y + ")");} }static class Line {public final int a;public final int b;public Line(int a, int b) {this.a = a;this.b = b;}// Inliningpublic int getY(int x) {return (a * x + b);} }優化的內聯版本將消除跳轉,參數l和x的發送以及y的返回:
private static void calcLine(int a, int b, int from, int to) {Line l = new Line(a, b);for (int x = from; x <= to; x++) {int y = (l.a * x + l.b);System.err.println("(" + x + ", " + y + ")");} }底線:內聯是一種超級有用的優化,但是只有在您使用最少的行數使方法盡可能簡單的情況下,內聯才會啟動。 復雜的方法不太可能內聯,因此這是您可以幫助JIT編譯器完成其工作的一點。
5.線程字段和線程本地存儲(TLS)
事實證明,線程字段比常規變量快得多。 線程對象存儲在實際的CPU寄存器中,從而使其字段成為非常有效的存儲空間。
使用線程本地存儲,您可以創建存儲在Thread對象上的變量。
以下是請求計數器的簡單示例,說明了如何訪問線程本地存儲:
private static void handleRequest() {if (counter.get() == null) {counter.set(0);}counter.set(counter.get() + 1);在相應的匯編代碼中,我們可以看到數據直接放置在寄存器中,與靜態類變量相比,可以更快地訪問數據:
0x00007fd71508b1ec: mov 0x1b0(%r15),%r10 ;*invokestatic currentThread; - java.lang.ThreadLocal::get@0 (line 143); - Opt::handleRequest@3 (line 70)0x00007fd71508b1f3: mov 0x50(%r10),%r11d ;*getfield threadLocals; - java.lang.ThreadLocal::getMap@1 (line 213); - java.lang.ThreadLocal::get@6 (line 144); - Opt::handleRequest@3 (line 70)底線:某些敏感數據類型可能更好地存儲在“線程本地存儲”中,以便更快地訪問和檢索。
最后的想法
JVMs JIT編譯器是Java平臺上引人入勝的機制之一。 它在不犧牲可讀性的情況下優化了代碼的性能。 不僅如此,除了內聯的“靜態”優化方法之外,它還基于代碼在實踐中的執行方式來做出決策。
我們希望您喜歡一些JVM最有用的優化技術,并且很高興在下面的評論部分中聽到您的想法。
Java開發人員? 優化您的生產監控。 請在所有已記錄的錯誤,警告和異常之后查看源代碼,調用堆棧和變量狀態- 嘗試Takipi 。
翻譯自: https://www.javacodegeeks.com/2016/08/java-steroids-5-super-useful-jit-optimization-techniques.html
總結
以上是生活随笔為你收集整理的类固醇上的Java:5种超级有用的JIT优化技术的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ANTLR入门:构建一种简单的表达语言
- 下一篇: linux服务器系统盘大小(linux