日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

作者:閑大賦
鏈接:https://my.oschina.net/xiandafu/blog/3067186

關于JMH,可以直接查看官網地址http://openjdk.JAVA.net/projects/code-tools/jmh/

1.3 JMH

1.3.1 使用JMH

通過手工編寫一個性能壓測程序有較多的問題

  • 不同需要性能比較方法放到一個虛擬機里調用,有可能會互相影響。最好的辦法是分成倆個獨立的進程運行,確保倆個對比方法不相互影響。
  • PerformaceAreaTest啟動后直接運行, 缺少預熱代過程。虛擬機在執行代碼過程中,會加載類,解釋執行,以及有可能的優化編譯。需要確保虛擬機進行了一定預熱運行,以保證測試的公平性,我們在運行PerformaceAreaTest2的時候,能看到第一次循環執行時間總是較長。可以參考第8章了解JIT
  • 為了避免環境影響造成的對結果統計不準,我們需要運行多次,取出平均成績
  • 需要從多個緯度統計方法的性能,統計冷啟動需要消耗的時間,統計OPS,TP99的功能。

JMH使用OPS來表示吞吐量,OPS,Opeartion Per Second,是衡量性能的重要指標,指得是每秒操作量。數值越大,性能越好。類似的概念還有TPS,表示每秒的事務完成量,QPS,每秒的查詢量。 如果對每次執行時間進行升序排序,取出總數的99%的最大執行時間作為TP99的值,TP99通常是衡量系統性能重要指標,他表示99%的請求的響應時間不超過某個值。比TP99更嚴格的事TP999,要求99.9%的請求不超過某個值

有什么工具能幫助我們統計性能優化后的效果,比如更方便的統計OPS,TP99等。同時,我們為了做調優,不必每次都自己寫一個測試程序

JMH,即Java Microbenchmark Harness,是專門用于代碼微基準測試的工具套件。主要是基于方法層面的基準測試,精度可以達到納秒級。當你定位到熱點方法,希望進一步優化方法性能的時候,就可以使用JMH對優化的結果進行量化的分析。

JMH 實現了JSR269規范,即注解處理器,能在編譯Java源碼的時候,識別的到需要處理的注解,如@Beanmark,JMH能根據@Beanmark的配置生成一系列測試輔助類。關于JSR269,本書11章詳細介紹. 流行開源Lombok 基于JSR269規范

開始是使用JMH,可以在工程里添加對JMH的依賴,添加如下

<dependency>

<groupId>org.openjdk.jmh</groupId>

<artifactId>jmh-core</artifactId>

<version>${jmh.version}</version>

</dependency>

<dependency>

<groupId>org.openjdk.jmh</groupId>

<artifactId>jmh-generator-annprocess</artifactId>

<version>${jmh.version}</version>

<scope>provided</scope>

</dependency>

${jmh.version} 為jmh最新版本,為1.0

我們編寫一個JMH測試類

@BenchmarkMode(Mode.Throughput)
@Warmup(iterations = 3)
@Measurement(iterations = 3, time = 5, timeUnit = TimeUnit.SECONDS)
@Threads(1)
@Fork(1)
@OutputTimeUnit(TimeUnit.SECONDS)
public class MyBenchmark {
 	@Benchmark
 public static void testStringKey(){
 //優化前的代碼
 }
 @Benchmark
 public static void testObjectKey(){
 //要測試的優化后代碼
 }
 public static void main(String[] args) throws RunnerException {
 Options opt = new OptionsBuilder()
 .include(MyBenchmark.class.getSimpleName())
 .build();
 new Runner(opt).run();
 }
}

MyBenchmark 有倆個需要比較的方法,都用 @Benchmark注解標識,MyBenchmark用了一系列注解,解釋如下

  • BenchmarkMode,使用模式,默認是Mode.Throughput,表示吞吐量,其他參數還有AverageTime,表示每次執行時間,SampleTime表示采樣時間,SingleShotTime表示只運行一次,用于測試冷啟動消耗時間,All表示統計前面的所有指標
  • Warmup 配置預熱次數,默認是每次運行1秒,運行10次,我們的例子是運行3次
  • Measurement 配置執行次數,本例是一次運行5秒,總共運行3次。在性能對比時候,采用默認1秒即可,如果我們用jvisualvm做性能監控,我們可以指定一個較長時間運行。
  • Threads 配置同時起多少個線程執行,默認值世 Runtime.getRuntime().availableProcessors(),本例啟動1個線程同時執行
  • Fork,代表啟動多個單獨的進程分別測試每個方法,我們這里指定為每個方法啟動一個進程。
  • OutputTimeUnit 統計結果的時間單元,這個例子TimeUnit.SECONDS,我們在運行后會看到輸出結果是統計每秒的吞吐量

我們在MyBenchmark添加需要的測試方法,如下

static AreaService areaService = new AreaService();
static PreferAreaService perferAreaService = new PreferAreaService();
static List<Area> data = buildData(20);
@Benchmark
public static void testStringKey(){
 areaService.buildArea(data);
}
@Benchmark
public static void testObjectKey(){
 perferAreaService.buildArea(data);
}
private static List<Area> buildData(int count){
 List<Area> list = new ArrayList<>(count);
 for(int i=0;i<count;i++){
 Area area = new Area(i,i*10);
 list.add(area);
 }
 return list;
}

因為MyBenchmark包含了一個main方法,我們可以直接在IDE里直接運行這個方法,有如下輸出

# Warmup: 3 iterations, 1 s each
# Measurement: 3 iterations, 5 s each
# Threads: 1 threads, will synchronize iterations
# Benchmark mode: Throughput, ops/time

以上輸出來自于我們的配置,第一行表示預熱3次,每次執行1秒,第二行表示運行3次,每次運行5秒,這部分的運行結果計入統計。第三行表示1個線程執行,第四行統計性能數據緯度是Throughput,吞吐量

緊接著會運行testObjectKey方法,有如下輸出

# Benchmark: com.ibeetl.code.ch01.test.MyBenchmark.testObjectKey
# Run progress: 0.00% complete, ETA 00:00:36
# Fork: 1 of 1
objc[68658]: Class JavaLaunchHelper is implemented in both /Library/Java/JavaVirtualMachines/jdk1.8.0_45.jdk/Contents/Home/jre/bin/java and /Library/Java/JavaVirtualMachines/jdk1.8.0_45.jdk/Contents/Home/jre/lib/libinstrument.dylib. One of the two will be used. Which one is undefined.
# Warmup Iteration 1: 1288302.671 ops/s
# Warmup Iteration 2: 3061587.202 ops/s
# Warmup Iteration 3: 1094970.828 ops/s
Iteration 1: 2491836.097 ops/s
Iteration 2: 2780362.118 ops/s
Iteration 3: 3621313.883 ops/s

這里的Fork表示子進程,我們只配置里一個,因此只有一個進程的執行結果,該進程包含預熱3次,每次1秒,以及運行3次,每次運行5秒,執行完testObjectKey方法后,會自動打印一個匯總信息

Result: 939996.216 ±(99.9%) 2012646.237 ops/s [Average]
 Statistics: (min, avg, max) = (813154.364, 939996.216, 1013607.616), stdev = 110319.932
 Confidence interval (99.9%): [-1072650.021, 2952642.453]

統計結果給出了多次測試后的最小值,最大值和均值,以及標準差 (stdev),置信區間(Confidence interval)

標準差(stdev)反映了數值相對于平均值得離散程度,置信區間是指由樣本統計量所構造的總體參數的估計區間。在統計學中,一個概率樣本的置信區間(Confidence interval)是對這個樣本的某個總體參數的區間估計

testStringKey的輸出與上面類似,這倆個比較方法執行完畢,會自動打印出一個性能對比數據表格

Benchmark Mode Samples Score Score error Units
c.i.c.c.t.MyBenchmark.testObjectKey thrpt 3 1976766.072 408421.217 ops/s
c.i.c.c.t.MyBenchmark.testStringKey thrpt 3 423788.869 222139.136 ops/s

Benchmark列表示這次測試對比的方法,Mode列表上結果的統計緯度,Samples列表示采樣次數,Samples=Fork*Iteration。Score是對這次評測的打分,對于testObjectKey,意味著他的OPS為每秒1976766,大約4倍testStringKey方法

Score Error 這里表示性能統計上的誤差,我們不需要關心這個數據,主要查看Score

可以修改統計緯度,比如修改為Mode.SampleTime,時間按照納秒統計

@BenchmarkMode(Mode.SampleTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
......
public class MyBenchmark {}

可以看到有一組如下統計

p( 0.0000) = 1992.000 ns/op
p(50.0000) = 2084.000 ns/op
p(90.0000) = 2464.000 ns/op
p(95.0000) = 3472.000 ns/op
p(99.0000) = 4272.000 ns/op
p(99.9000) = 17481.920 ns/op
p(99.9900) = 80659.840 ns/op
p(99.9990) = 562593.690 ns/op
p(99.9999) = 745472.000 ns/op

可以看到90%的調用,是在2464納秒內完成,99%的調用都是在4272納秒完成的.

1.3.2 JMH常用設置

在這個例子,我們性能測試所依賴的對象areaService,perferAreaService 恰好是線程安全的,大多數時候性能測試方法都會引用一些外部實例對象,考慮到多線程測試訪問這些實例對象,JMH要求必須為這些變量申明是Thread 內生效,還是整個BeanMark使用。如果是前者,JMH會為每個線程構建一個新的實例,后者則所有測試都共享這個變量,JMH用@State注解來說明對象的生命周期,@State注解作用在類上,比如,在MyBenchmark例子里,我們可以改成如下例子

@State(Scope.Benchmark)
public static class SharedPara{
 AreaService areaService = new AreaService();
 PreferAreaService perferAreaService = new PreferAreaService();
 List<Area> data = buildData(20);
 private List<Area> buildData(int count){
 //忽略其他代碼
 }
}
@Benchmark
public void testStringKey(SharedPara para){
 para.areaService.buildArea(para.data);
}
@Benchmark
public void testObjectKey(SharedPara para){
 para.perferAreaService.buildArea(para.data);
}

必須申明一公共靜態內部類,該類包含了我們需要使用的實例對象,并在該類用@State注解表明這個對象是Thread的還是BeanchMark范圍內使用。在這個例子里,因為配置為Scope.Benchmark,JMH在整個性能測試過程中,只構造一個SharedPara實例,SharedPara 作為參數傳入每個待測試的方法。

也可以不使用內部類,直接使用申明性能測試的類,在類上使用@State注解

@State(Scope.Benchmark)
public class MyBenchmarkStateSimple {
 AreaService areaService = new AreaService();
 PreferAreaService perferAreaService = new PreferAreaService();
 List<Area> data = buildData(20);
 //忽略其他代碼
}

@Setup 和 @TearDown 是一對注解,作用于方法上,前者用于測試前的初始化工作,后者用于回收某些資源,比如壓測前需要準備一些數據

@State(Scope.Benchmark)
public class ScriptEngineBeanchmrk {
 String script = null;
 @Benchmark
 public void nashornTest(){
		// ... 測試方法
 }
 
 @Setup
 public void loadScriptFromFile(){
		//加載一個測試腳本
 }
}

@Level 用于控制 @Setup,@TearDown 的調用時機,有如下含義

  • Level.Tiral: 運行每個性能測試的時候執行,推薦的方式。
  • Level.Iteration, 每次迭代的時候執行
  • Level.Invocation,每次調用方法的時候執行,這個選項需要謹慎使用。

JMH提供了Runner類能運行Benchmark類

public static void main(String[] args) throws RunnerException {
 Options opt = new OptionsBuilder()
 .include(MyBenchmark.class.getSimpleName())
 .build();
 new Runner(opt).run();
}

include接受一個字符串表達式,表示需要測試的類和方法,如上例子測試所有方法MyBenchmark。如下例子則只測試方法名字包含“testObjectKey“的方法

include(MyBenchmark.class.getSimpleName()+".*testObjectKey*")

OptionsBuilder包含了多個方法用于配置性能測試,可以指定循環次數,預熱次數等,如下例子會用4個子進程做性能測試,每個進程預熱一次,執行5次迭代

public static void main(String[] args) throws RunnerException {
 Options opt = new OptionsBuilder()
 .include(MyBenchmark.class.getSimpleName())
 .forks(4)
 .warmupIterations(1)
 .measurementIterations(5)
 .build();
 new Runner(opt).run();
}

截至到目前為止,JMH都是通過一個main方法在IDE里執行,更為通常情況,JMH推薦使用單獨的一個Maven工程來執行性能測試而不要放到業務工程里。可以通過maven archetype:generate 命令來生成一個心得JMH Maven工程。

mvn archetype:generate
 -DinteractiveMode=false
 -DarchetypeGroupId=org.openjdk.jmh
 -DarchetypeArtifactId=jmh-java-benchmark-archetype
 -DgroupId=code.ibeetl.com
 -DartifactId=first-benchmark
 -Dversion=1.0

為了閱讀方便,分成幾行,如上命令行應該放到一行執行,執行完畢后,生成了一個maven工程,maven工程僅僅包含了一個 MyBenchmark 例子。

package org.sample;
import org.openjdk.jmh.annotations.Benchmark;
public class MyBenchmark {
 @Benchmark
 public void testMethod() {
 // place your benchmarked code here
 }
}

我們可以修改MyBenchmark,添加我們需要測試的代碼, 現在,可以創建一個性能測試的jar文件,通過運行如下maven命令

mvn clean install

命令會在target目錄下生成一個benchmarks.jar,包含了運行性能測試所需的任何東西,在命令行運行如下命令

java -jar target/benchmarks.jar MyBenchmark

JMH將會被啟動,默認情況下運行MyBenchmark類里的所有被@Benchmark標注方法

有些性能測試需要了解不同輸入參數的性能,比如對于模板引擎的性能測試中,考慮到字節流輸出和字符流輸出

@Param({"1","2","3"})
int outputType;
@Benchmark
public String benchmark() throws TemplateException, IOException {
 if(outputType==3){
			return doStream();
 }else if(outputType==2) {
 return doCharStream()
 }else{
 return doString();
 }
 
}

JMH會分別賦值outpuType為1,2,3后,在各自測試一次,會輸出如下

Benchmark	 (outputType)	Score	 Units
Beetl.benchmark	 1	 44977.421	ops/s
Beetl.benchmark	 2	 34931.724	ops/s
Beetl.benchmark	 3	 59175.106	ops/s

1.3.3 注意事項

編寫JHM代碼,需要考慮到虛擬機的優化,而使得測試失真,如下measureWrong代碼就是所謂的Dead-Code代碼

@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class JMHSample_08_DeadCode {
 private double x = Math.PI;
 @Benchmark
 public void baseline() {
 //基準
 }
 @Benchmark
 public void measureWrong() {
 //虛擬機會優化掉這部分,性能同baseline
 Math.log(x);
 }
 @Benchmark
 public double measureRight() {
 // 真正的性能測試
 return Math.log(x);
 }
}

測試結果如下

Benchmark Mode Score Units

c.i.c.c.c.i.c.c.j.JMHSample_08_DeadCode.baseline avgt 0.358 ns/op

c.i.c.c.c.i.c.c.j.JMHSample_08_DeadCode.measureRight avgt 24.605 ns/op

c.i.c.c.c.i.c.c.j.JMHSample_08_DeadCode.measureWrong avgt 0.366 ns/op

在測試measureWrong方法,JIT能推測出方法體可以被優化調而不影響系統,measureRight因為定義了返回值,JIT不會優化。

下一個是關于常量折疊,JIT認為方法計算結果為常量,從而優化直接返回常量給調用者

 private double x = Math.PI;
 private final double wrongX = Math.PI;
 @Benchmark
 public double baseline() {
 // 基準測試
 return Math.PI;
 }
 @Benchmark
 public double measureWrong_1() {
 // JIT認為是個常量
 return Math.log(Math.PI);
 }
 @Benchmark
 public double measureWrong_2() {
 // JIT認為方法調用結果是個常量.
 return Math.log(wrongX);
 }
 @Benchmark
 public double measureRight() {
 // 正確的測試
 return Math.log(x);
 }

如下是測試結果

Benchmark Mode Score Units

c.i.c.c.c.i.c.c.j.JMHSample_10_ConstantFold.baseline avgt 1.175 ns/op

c.i.c.c.c.i.c.c.j.JMHSample_10_ConstantFold.measureRight avgt 25.805 ns/op

c.i.c.c.c.i.c.c.j.JMHSample_10_ConstantFold.measureWrong_1 avgt 1.116 ns/op

c.i.c.c.c.i.c.c.j.JMHSample_10_ConstantFold.measureWrong_2 avgt 1.031 ns/op

考慮到inline對性能影響很大,JMH支持 @CompilerControl來控制是否允許內聯

public class Inline {
 int x=0,y=0;
 @Benchmark
 @CompilerControl(CompilerControl.Mode.DONT_INLINE)
 public int add(){
 return dataAdd(x,y);
 }
 @Benchmark
 public int addInline(){
 return dataAdd(x,y);
 }
 private int dataAdd(int x,int y){
 return x+y;
 }
 @Setup
 public void init() {
 x = 1;
 y = 2;
 }
}

add和addInline方法都會調用dataAdd方法,前者使用CompilerControl類,可以用在方法或者類上,來提供編譯選項

  • DONT_INLINE,調用方法不內聯
  • INLINE,調用方法內聯
  • BREAK,插入一個調試斷點(TODO,如何調試,參考11章)
  • PRINT,打印方法被JIT編譯后的機器碼信息

開發人員可能覺得上面的測試,add方法太簡單,會習慣性的在add方法里方一個循環,以減少JMH調用add方法的成本。JMH不建議這么做,因為JIT會實際上對這種循環會做優化,以消除循環調用成本。如下是個例子可以看到循環測試結果不準確

int x = 1;
int y = 2;
/** 正確測試
*/
@Benchmark
public int measureRight() {
 return (x + y);
}
private int reps(int reps) {
 int s = 0;
 for (int i = 0; i < reps; i++) {
 s += (x + y);
 }
 return s;
}
@Benchmark
@OperationsPerInvocation(1)
public int measureWrong_1() {
 return reps(1);
}
@Benchmark
@OperationsPerInvocation(10)
public int measureWrong_10() {
 return reps(10);
}
@Benchmark
@OperationsPerInvocation(100)
public int measureWrong_100() {
 return reps(100);
}
@Benchmark
@OperationsPerInvocation(1000)
public int measureWrong_1000() {
 return reps(1000);
}

注解OperationsPerInvocation 告訴JMH統計性能的時候需要做修正,比如@OperationsPerInvocation(10)調用了10次。

性能測試結果如下

編寫性能測試的一個好習慣是先編寫一個單元測試用例,以確保性能測試準確性,x Benchmark Mode Score Units c.i.c.c.c.i.c.c.j.JMHSample_11_Loops.measureRight avgt 1.114 ns/op c.i.c.c.c.i.c.c.j.JMHSample_11_oops.measureWrong_1 avgt 1.057 ns/op c.i.c.c.c.i.c.c.j.JMHSample_11_Loops.measureWrong_10 avgt 0.139 ns/op c.i.c.c.c.i.c.c.j.JMHSample_11_Loops.measureWrong_100 avgt 0.018 ns/op c.i.c.c.c.i.c.c.j.JMHSample_11_Loops.measureWrong_1000 avgt 0.035 ns/op java

可以看到,測試方法里使用循環,會促使JIT進行優化,做循環消除(參考第8章JIT TODO)

1.3.4 單元測試

無論是編寫JMH,或者其他性能測試程序,好習慣是先編寫一個單元測試用例,以確保性能測試方法的準確性,對于1.3.4的Inline類,可以先編寫一個單元測試用例,確保add和addInline返回正確結果

public class InLineTestJunit {
 @Test
 public void test(){
 Inline inline = new Inline();
 inline.init();
 //期望結果
 int expectd = inline.x+inline.y;
 int ret = inline.add();
 int ret2 = inline.addInline();
 Assert.assertEquals(expectd,ret);
 Assert.assertEquals(expectd,ret2);
 }
}

在JMH工程調用maven install 生成測試代碼的時候,會進行單元測試,從而保證測試結果的準確

分享到:
標簽:性能 優化 Java
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定