成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

JMH性能測試,試試你代碼的性能如何

開發 后端
最近在研究一些基礎組件實現的時候遇到一個問題,關于不同技術的運行性能比對該如何去實現。

[[442787]]

最近在研究一些基礎組件實現的時候遇到一個問題,關于不同技術的運行性能比對該如何去實現。

什么是性能比對呢?

舉個簡單的栗子🌰 來說:假設我們需要驗證String,StringBuffer,StringBuilder三者在使用的時候,希望能夠通過一些測試來比對它們的性能開銷。下邊我羅列出最簡單的測試思路:

for循環比對

這種測試思路的特點:簡單直接 

  1. public class TestStringAppendDemo {  
  2.     public static void testStringAdd() {  
  3.         long begin = System.currentTimeMillis();  
  4.         String item = new String();  
  5.         for (int i = 0; i < 100000; i++) {  
  6.             itemitem = item + "-";  
  7.         }  
  8.         long end = System.currentTimeMillis();  
  9.         System.out.println("StringBuffer 耗時:" + (end - begin) + "ms");  
  10.     }  
  11.     public static void testStringBufferAdd() {  
  12.         long begin = System.currentTimeMillis();  
  13.         StringBuffer item = new StringBuffer();  
  14.         for (int i = 0; i < 100000; i++) {  
  15.             itemitem = item.append("-");  
  16.         }  
  17.         long end = System.currentTimeMillis();  
  18.         System.out.println("StringBuffer 耗時:" + (end - begin) + "ms");  
  19.     }  
  20.     public static void testStringBuilderAdd() {  
  21.         long begin = System.currentTimeMillis();  
  22.         StringBuilder item = new StringBuilder();  
  23.         for (int i = 0; i < 100000; i++) {  
  24.             itemitem = item.append("-");  
  25.         }  
  26.         long end = System.currentTimeMillis();  
  27.         System.out.println("StringBuilder 耗時:" + (end - begin) + "ms");  
  28.     }  
  29.     public static void main(String[] args) {  
  30.             testStringAdd();  
  31.             testStringBufferAdd();  
  32.             testStringBuilderAdd();  
  33.     }  

不知道你在平時工作中是否經常會這么做,雖然說通過簡單的for循環執行來看,我們確實能夠較好地給出誰強誰弱的這種結論,但是比對的結果并不精準。因為Java程序的運行時有可能會越跑越快的!

代碼越跑越快

看到這里你可能會有些疑惑,Java程序不是在啟動之前都編譯成了統一的字節碼么,難道在字節碼翻譯為機器代碼的過程中還有什么不為人知的優化處理手段?

下邊我們來觀察這么一段測試程序: 

  1. public static void testStringAdd() {  
  2.         long begin = System.currentTimeMillis();  
  3.         String item = new String();  
  4.         for (int i = 0; i < 100000; i++) {  
  5.             itemitem = item + "-";  
  6.         }  
  7.         long end = System.currentTimeMillis();  
  8.         System.out.println("String 耗時:" + (end - begin) + "ms");  
  9.     }  
  10.     //循環20次執行同一個方法  
  11.     public static void main(String[] args) {  
  12.         for(int i=0;i<20;i++){  
  13.             testStringAdd();  
  14.         }  
  15.     } 

執行的程序耗時打印在了控制臺上:

20次的重復調用之后,發現首次和最后一次調用幾乎存在5倍的差異。看來代碼運行越跑越快是存在的了,但是為什么會有這種現象發生呢?

這里我們需要了解一項叫做JIT的技術。

JIT技術

在介紹JIT技術之前,需要先進行些相關知識的補充鋪墊。

解釋型語言

解釋型語言,是在運行的時候才將程序翻譯成 機器語言 。解釋型語言的程序不需要在運行前提前做編譯工作,在運行程序的時候才翻譯,解釋器負責在每個語句執行的時候解釋程序代碼。這樣解釋型語言每執行一次就要“翻譯”一次,效率比較低。代表語言:PHP。

編譯型語言

在程序執行之前,提前就將程序編譯成機器代碼,這樣后續機器在運行的時候就不需要額外去做翻譯的工作,效率會相對較高。語言代表:C,C++。

而我們本文重點研究的是Java語言,我個人認為這是一門既具備解釋特點又具備編譯特點的高級語言。

JVM是Java一次編譯,跨平臺執行的基礎。當Java被編譯為字節碼形式的.class文件之后,他可以在任意的JVM上運行。

PS: 這里說的編譯,主要是指前端編譯器。

前端編譯器

將.java文件編譯為JVM可執行的.class字節碼文件,即javac,主要職責包括:詞法、語法分析,填充符號表,語義分析,字節碼生成。輸出為字節碼文件,也可以理解為是中間表達形式(稱為IR:Intermediate Representation)。這時候的編譯結果就是我們常見的xxx.class文件。

后端編譯器

在程序運行期間將字節碼轉變成機器碼,通過前端編譯器和后端編譯器的組合使用,通常就是被我們稱之為混合模式,如 HotSpot 虛擬機自帶的解釋器還有 JIT(Just In Time Compiler)編譯器(分 Client 端和 Server 端),其中JIT還會將中間表達形式進行一些優化。

所以一份xxx.java的文件實際在執行過程中會按照如下流程執行,首先經過前端解釋器轉換為.class格式的字節碼,再通過后端編譯器將其解釋為機器能夠識別的機器代碼。最后再由機器去執行計算。

真的就這么簡單嗎?

還記得我在上邊貼出的那段測試代碼嗎,首次執行和最后執行的性能差異如此巨大,其實是在后端編譯器處理的過程中加入優化的手段。

在編譯時,主要是將java源代碼文件編譯為統一的字節碼,但是編譯成的字節碼并不能直接運行,而是需要通過JVM讀取運行。JVM中的后端解釋器就是將.class文件一行一行翻譯之后再運行,翻譯就是轉換成當前機器可以運行的機器碼,它不會一次性把整個文件都翻譯過來,而是翻譯一句,執行一句,再翻譯,再執行,所以解釋器的程序運行起來會比較慢,每次都要解釋之后再執行。所以有些時候,我們想是否可以把解釋之后的內容緩存起來,這樣不就可以直接運行了?但是,如果每段代碼都要緩存起來,例如僅僅執行一次的代碼也緩存起來,這樣太浪費內存了。所以,引入一個新的運行時編譯器,JIT來解決這些問題,加速熱點代碼的執行。

引入JIT技術之后,代碼的執行過程是怎樣的?

在引入了JIT技術之后,一份Java程序的代碼執行流程就會變成了下邊這種類型。首先通過前端編譯器轉變為字節碼文件,然后再判斷對應的字節碼文件是否有被提前處理好存放在code cache中。如果有則可以直接執行對應的機器代碼,如果沒有則需要進行判斷是否有必要進行JIT技術優化(判斷邏輯的細節后邊會講),如果有必要優化,則會將優化后的機器碼也存放到code cache中,否則則是會一邊執行一邊翻譯為機器代碼。

怎樣的代碼才會被識別為熱點代碼呢?

在JVM中會設置一個閾值,當某段代碼塊在一定時間內被執行的次數超過了這個閾值,則會被存放進code cache中。

如何驗證:

建立一個測試用的代碼Demo,然后設置JVM參數:

-XX:CompileThreshold=500 -XX:+PrintCompilation 

  1. public class TestCountDemo {  
  2.     public static void test() {  
  3.         int a = 0 
  4.     }  
  5.    public static void main(String[] args) throws InterruptedException {  
  6.         for (int i = 0; i < 600; i++) {  
  7.             test();  
  8.         }  
  9.         TimeUnit.SECONDS.sleep(1);  
  10.     }  

接下來專心觀察啟動程序之后的編譯信息記錄:

截圖解釋:

第一列693表示系統啟動到編譯完成時的毫秒數。

第二列43表示編譯任務的內部ID,一般是一個自增的值。

第三列為空,描述代碼狀態的5個屬性。

  •  %:是一個OSR(棧上替換)。
  •  s:是一個同步方法。
  •  !:方法有異常處理塊。
  •  b:阻塞模式編譯。
  •  n:是本地方法的一個包裝。

第四列3表示編譯級別,0表示沒有編譯而是使用解釋器,1,2,3表示使用C1編譯器(client),4表示使用C2編譯器(server),級別越高編譯生成的機器碼質量越好,編譯耗時也越長。

最后一列表示了方法的全限定名和方法的字節碼長度。

從實驗來看,當for循環的次數一旦超過了預期設置的閾值,則會提前使用后端編譯器將代碼緩存到code cache中。

即時編譯極大地提高了Java程序的運行速度,而且跟靜態編譯相比,即時編譯器可以選擇性地編譯熱點代碼,省去了很多編譯時間,也節省很多的空間。目前,即時編譯器已經非常成熟了,在性能層面甚至可以和編譯型語言相比。不過在這個領域,大家依然在不斷探索如何結合不同的編譯方式,使用更加智能的手段來提升程序的運行速度。

還記得我在文章開頭所提出的幾個問題嗎~~既然我們了解了Jvm底層具備了這些優化的技能,那么如何才能更加準確高效地去檢測一段程序的性能呢?

基于JMH來實踐代碼基準測試

JMH是Java Microbenchmark Harness的簡稱,一個針對Java做基準測試的工具,是由開發JVM的那群人開發的。想準確的對一段代碼做基準性能測試并不容易,因為JVM層面在編譯期、運行時對代碼做很多優化,但是當代碼塊處于整個系統中運行時這些優化并不一定會生效,從而產生錯誤的基準測試結果,而這個問題就是JMH要解決的。

關于如何使用JMH在網上有很多的講解案例,這些入門的資料大家可以自行去搜索。本文主要講解在使用JMH測試的時候需要注意到的一些細節點:

常用的基本注解以及其具體含義

一般我們會將測試所使用的注解都標注在測試類的頭部,常用到的測試注解有以下幾種: 

  1. /**  
  2.  * 吞吐量測試 可以獲取到指定時間內的吞吐量  
  3.  *  
  4.  * Throughput 可以獲取一秒內可以執行多少次調用  
  5.  * AverageTime 可以獲取每次調用所消耗的平均時間  
  6.  * SampleTime 隨機抽樣,隨機抽取結果的分布,最終是99%%的請求在xx秒內  
  7.  * SingleShotTime 只允許一次,一般用于測試冷啟動的性能  
  8.  */  
  9. @BenchmarkMode(Mode.Throughput) 
  10. /**  
  11.  * 如果一段程序被調用了好幾次,那么機器就會對其進行預熱操作,  
  12.  * 為什么需要預熱?因為 JVM 的 JIT 機制的存在,如果某個函數被調用多次之后,JVM 會嘗試將其編譯成為機器碼從而提高執行速度。所以為了讓 benchmark 的結果更加接近真實情況就需要進行預熱。  
  13.  */  
  14. @Warmup(iterations = 3 
  15. /**  
  16.  * iterations 每次測試的輪次  
  17.  * time 每輪進行的時間長度  
  18.  * timeUnit 時長單位  
  19.  */  
  20. @Measurement(iterations = 10time = 5timeUnit = TimeUnit.SECONDS)  
  21. /**  
  22.  * 測試的線程數,一般是cpu*2  
  23.  */  
  24. @Threads(8)  
  25. /**  
  26.  * fork多少個進程出來測試  
  27.  */  
  28. @Fork(2)  
  29. /**  
  30.  * 這個比較簡單了,基準測試結果的時間類型。一般選擇秒、毫秒、微秒。  
  31.  */  
  32. @OutputTimeUnit(TimeUnit.MILLISECONDS) 

如果不喜歡使用注解的方式也可以通過在啟動入口中通過硬編碼的形式設置: 

  1. public static void main(String[] args) throws RunnerException {  
  2.         //配置進行2輪熱數 測試2輪 1個線程  
  3.         //預熱的原因 是JVM在代碼執行多次會有優化  
  4.         Options options = new OptionsBuilder().warmupIterations(2).measurementBatchSize(2)  
  5.                 .forks(1).build();  
  6.         new Runner(options).run();  
  7.     } 

如果要對某項方法進行JMH測試的話,通常會對該方法的頭部加入@Benchmark注解。例如下邊這段: 

  1. @Benchmark  
  2.     public String testJdkProxy() throws Throwable {  
  3.         String content = dataService.sendData("test");  
  4.         return content;  
  5.     } 

JMH的一些坑

所有方法都應該要有返回值

例如這么一段測試案例: 

  1. package org.idea.qiyu.framework.jmh.demo;  
  2. import org.openjdk.jmh.annotations.*;  
  3. import org.openjdk.jmh.runner.Runner;  
  4. import org.openjdk.jmh.runner.RunnerException;  
  5. import org.openjdk.jmh.runner.options.Options;  
  6. import org.openjdk.jmh.runner.options.OptionsBuilder;  
  7. import java.util.concurrent.TimeUnit;  
  8. import static org.openjdk.jmh.annotations.Mode.AverageTime;  
  9. import static org.openjdk.jmh.annotations.Mode.Throughput;  
  10. /**  
  11.  * JMH基準測試  
  12.  */  
  13. @BenchmarkMode(Throughput)  
  14. @Fork(2)  
  15. @Warmup(iterations = 4 
  16. @Threads(4)  
  17. @OutputTimeUnit(TimeUnit.MILLISECONDS)  
  18. public class JMHHelloWord { 
  19.     @Benchmark  
  20.     public void baseMethod() {  
  21.     }  
  22.     @Benchmark  
  23.     public void measureWrong() {  
  24.         String item = "" 
  25.         itemitem = item + "s";  
  26.     }  
  27.     @Benchmark  
  28.     public String measureRight() {  
  29.         String item = "" 
  30.         itemitem = item + "s";  
  31.         return item;  
  32.     }  
  33.     public static void main(String[] args) throws RunnerException {  
  34.         Options options = new OptionsBuilder().  
  35.                 include(JMHHelloWord.class.getName()).  
  36.                 build();  
  37.         new Runner(options).run();  
  38.     }  

其實baseMethod和measureWrong兩個方法從代碼功能角度看來,并沒有什么區別,因為調用它們兩者對于調用方本身并沒有造成什么影響,而且measureWrong函數中還存在著無用代碼塊,所以JMH會對內部的代碼進行“死碼消除”的處理。

通過測試會發現,其實baseMethod和measureWrong的吞吐性結果差別不大。反而再比對measureWrong和measureRight兩個方法,后者只是加入了一個return關鍵字,JMH就能很好地去測算它的整體性能。

關于什么是“死碼消除”,我在這里貼出一段維基百科上的介紹,感興趣的讀者可以自行前往閱讀:

https://zh.wikipedia.org/wiki/%E6%AD%BB%E7%A2%BC%E5%88%AA%E9%99%A4

不要在Benchmark內部加入循環的代碼

關于這一點我們可以通過一段案例來進行測試,代碼如下: 

  1. package org.idea.qiyu.framework.jmh.demo;  
  2. import org.openjdk.jmh.annotations.*;  
  3. import org.openjdk.jmh.runner.Runner;  
  4. import org.openjdk.jmh.runner.RunnerException;  
  5. import org.openjdk.jmh.runner.options.Options;  
  6. import org.openjdk.jmh.runner.options.OptionsBuilder;  
  7. import java.util.concurrent.TimeUnit;  
  8. /**  
  9.  * @Author linhao  
  10.  * @Date created in 10:20 上午 2021/12/19  
  11.  */  
  12. @BenchmarkMode(Mode.AverageTime)  
  13. @Fork(1)  
  14. @Threads(4)  
  15. @Warmup(iterations = 1 
  16. @OutputTimeUnit(TimeUnit.MILLISECONDS)  
  17. public class ForLoopDemo {  
  18.     public int reps(int count) {  
  19.         int sum = 0
  20.         for (int i = 0; i < count; i++) {  
  21.             sumsum = sum + count;  
  22.         }  
  23.         return sum;  
  24.     }  
  25.     @Benchmark  
  26.     @OperationsPerInvocation(1)  
  27.     public int test_1() {  
  28.         return reps(1);  
  29.     }  
  30.     @Benchmark  
  31.     @OperationsPerInvocation(10)  
  32.     public int test_2() {  
  33.         return reps(10);  
  34.     }  
  35.     @Benchmark  
  36.     @OperationsPerInvocation(100)  
  37.     public int test_3() {  
  38.         return reps(100);  
  39.     }  
  40.     @Benchmark  
  41.     @OperationsPerInvocation(1000)  
  42.     public int test_4() {  
  43.         return reps(1000);  
  44.     }  
  45.     @Benchmark  
  46.     @OperationsPerInvocation(10000)  
  47.     public int test_5() {  
  48.         return reps(10000);  
  49.     }  
  50.     @Benchmark 
  51.     @OperationsPerInvocation(100000)  
  52.     public int test_6() {  
  53.         return reps(100000);  
  54.     }  
  55.     public static void main(String[] args) throws RunnerException {  
  56.         Options options = new OptionsBuilder()  
  57.                 .include(ForLoopDemo.class.getName())  
  58.                 .build();  
  59.         new Runner(options).run();  
  60.     }  

測試出來的結果顯示:

循環越多,反而得分越低,這一結果反而越來越不可信。

關于為什么在Benchmark中跑循環代碼會出現這類不可信的情況,我在網上搜了一下技術文章,大致歸納為以下:

  •  循環展開
  •  JIT & OSR 對循環的優化

感興趣的朋友可以自行去深入了解,這里我就不做過多介紹了。

通過這個實驗可以發現,以后進行Benchmark的性能測試過程中,盡量能不跑循環就不要跑循環,如果真的要跑循環,可以看下官方的這個用例:

https://github.com/lexburner/JMH-samples/blob/master/src/main/java/org/openjdk/jmh/samples/JMHSample_34_SafeLooping.java

Fork注解中的進程數一定要大于0

這個是我通過實驗發現的,如果設置為小于0的參數會發現跑出來的效果和預期的大大相反,具體原因還不太清楚。

測試結果報告的參數解釋

最后是關于如何閱讀JMH的測試報告,這里的這份報告是上邊講解的代碼案例中的測試結果。由于報告的內容量比較大,所以這里只挑報告的結果來進行講解: 

  1. Benchmark                   Mode  Cnt         Score        Error   Units  
  2. JMHHelloWord.baseMethod    thrpt   10  14343234.962 ± 585752.043  ops/ms  
  3. JMHHelloWord.measureRight  thrpt   10    260749.234 ±   5324.982  ops/ms  
  4. JMHHelloWord.measureWrong  thrpt   10    524449.863 ±   8330.106  ops/ms 

從報告的左往右開始介紹起:

  •  Benchmark 就是對應的測試方法。
  •  Mode 測試的模式。
  •  Cnt 循環了多少次。
  •  Score 是指測試的得分,這里因為選擇了以thrpt的模式進行測試,所以分值越高表示吞吐率越高。
  •  Error 代表并不是表示執行用例過程中出現了多少異常,而是指這個Score的精度可能存在誤差,所以前邊還有個± 的符號。

關于Error的解釋,在stackoverflow中也有解釋:

https://codereview.stackexchange.com/questions/90886/jmh-benchmark-metrics-evaluation

如果你希望報告不是輸出在控制臺,而是可以匯總到一份文檔中,可以通過啟動指令去設置,例如: 

  1. public static void main(String[] args) throws RunnerException {  
  2.         Options options = new OptionsBuilder()  
  3.                 .include(StringBuilderBenchmark.class.getSimpleName())  
  4.                 .output("/Users/linhao/IdeaProjects/qiyu-framework-gitee/qiyu-framework/qiyu-framework-jmh/log/test.log")  
  5.                 .build();  
  6.         new Runner(options).run();  
  7.     }  

 

責任編輯:龐桂玉 來源: Java知音
相關推薦

2023-05-12 13:21:12

JMHJava程序

2021-07-08 14:59:05

JMHMongodb數據

2020-06-10 10:40:03

JavaJMH字符串

2021-03-18 07:52:42

代碼性能技巧開發

2016-09-23 16:36:25

LinuxPCPhoronix

2025-01-27 11:52:23

2024-12-23 08:10:00

Python代碼性能代碼

2014-04-25 09:02:17

LuaLua優化Lua代碼

2019-09-29 16:17:25

Java代碼性能編程語言

2013-06-27 10:34:08

準備性能測試數據

2011-03-15 16:34:36

Iptables性能

2021-06-30 10:16:54

微服務架構測試

2021-11-30 10:38:09

splitStringTokenJava

2024-03-20 08:00:00

軟件開發Java編程語言

2013-08-15 14:10:24

云主機磁盤IO

2013-05-08 09:31:32

MangoDB

2013-12-25 10:32:41

MySQL性能測試

2023-09-18 16:14:35

性能測試開發

2017-08-10 14:04:25

前端JavaScript函數性能

2010-06-22 09:06:36

Visual Stud
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 国产精品一区二区三区四区五区 | 在线婷婷 | 国产亚洲精品久久久久动 | 国产无套一区二区三区久久 | 久久精品视频在线播放 | 亚洲视频一区二区三区 | 午夜精品一区二区三区免费视频 | 国产精品亚洲一区 | 日本不卡一区二区三区在线观看 | 精品毛片 | 欧美在线观看一区 | 成人影院av | 美人の美乳で授乳プレイ | 小h片免费观看久久久久 | 黄色一级免费观看 | 国产最新精品视频 | 超碰天天| 韩日一区二区三区 | 精品一区二区久久久久久久网精 | 欧美精品二区三区 | 99精品国产一区二区青青牛奶 | 久久中文字幕一区 | 欧美亚洲成人网 | 中文字幕在线视频免费视频 | 一区二区三区四区在线免费观看 | 欧洲精品码一区二区三区免费看 | 国产亚洲欧美另类一区二区三区 | 999久久久久久久久6666 | 国产精品免费一区二区三区四区 | 欧美伊人影院 | 欧美三级三级三级爽爽爽 | 欧美精品在欧美一区二区少妇 | 色视频网站| 欧美中文一区 | 日韩播放 | 欧美成视频在线观看 | 金莲网 | 天天射网站 | 欧美成人a∨高清免费观看 老司机午夜性大片 | 亚洲精品久久久蜜桃 | 久草热8精品视频在线观看 午夜伦4480yy私人影院 |