逝者如斯夫

          靜而思之
          數(shù)據(jù)加載中……

          JMH(Java Micro Benchmark) 簡介

          ?

          JMH簡介

          本文由 ImportNew - hejiani 翻譯自
          java-performance

          JMH是新的microbenchmark(微基準測試)框架(2013年首次發(fā)布)。與其他眾多框架相比它的特色優(yōu)勢在于,它是由Oracle實現(xiàn)JIT的相同人員開發(fā)的。特別是我想提一下Aleksey Shipilev和他優(yōu)秀的博客文章。JMH可能與最新的Oracle JRE同步,其結(jié)果可信度很高。

          JMH的示例鏈接

          使用JMH僅需滿足2個必要條件(其他所有都是建議選項):

          • 設(shè)置jmh-core的maven依賴
          • 使用@GenerateMicroBenchmark注解測試方法

          本文將主要介紹JMH的基本規(guī)則和功能。第二篇文章將介紹JMH分析器

          如何運行

          在pom文件中加入依賴(在Maven Central查看jmh-core的最新版本):

          <dependencies>
              <dependency>
              <groupId>org.openjdk.jmh</groupId>
              <artifactId>jmh-core</artifactId>
              <version>0.4.2</version>
              </dependency>
          </dependencies>
          

          生成一個包含main方法的Java類。main方法中加入以下代碼:

              Options opt = new OptionsBuilder()
                          .include(".*" + YourClass.class.getSimpleName() + ".*")
                          .forks(1)
                          .build();
              new Runner(opt).run();
          

          測試方法使用@GenerateMicroBenchmark注解。運行該類。

          測試模式

          測試方法上@BenchmarkMode注解表示使用特定的測試模式:

          名稱描述
          Mode.Throughput計算一個時間單位內(nèi)操作數(shù)量
          Mode.AverageTime計算平均運行時間
          Mode.SampleTime計算一個方法的運行時間(包括百分位)
          Mode.SingleShotTime方法僅運行一次(用于冷測試模式)。或者特定批量大小的迭代多次運行(具體查看后面的“@Measurement“注解)——這種情況下JMH將計算批處理運行時間(一次批處理所有調(diào)用的總時間)
          這些模式的任意組合可以指定這些模式的任意組合——該測試運行多次(取決于請求模式的數(shù)量)
          Mode.All所有模式依次運行

          時間單位

          使用@OutputTimeUnit指定時間單位,它需要一個標準Java類型java.util.concurrent.TimeUnit作為參數(shù)。可是如果在一個測試中指定了多種測試模式,給定的時間單位將用于所有的測試(比如,測試SampleTime適宜使用納秒,但是throughput使用更長的時間單位測量更合適)。

          測試參數(shù)狀態(tài)

          測試方法可能接收參數(shù)。這需要提供單個的參數(shù)類,這個類遵循以下4條規(guī)則:

          • 有無參構(gòu)造函數(shù)(默認構(gòu)造函數(shù))
          • 是公共類
          • 內(nèi)部類應(yīng)該是靜態(tài)的
          • 該類必須使用@State注解

          @State注解定義了給定類實例的可用范圍。JMH可以在多線程同時運行的環(huán)境測試,因此需要選擇正確的狀態(tài)。

          名稱描述
          Scope.Thread默認狀態(tài)。實例將分配給運行給定測試的每個線程。
          Scope.Benchmark運行相同測試的所有線程將共享實例。可以用來測試狀態(tài)對象的多線程性能(或者僅標記該范圍的基準)。
          Scope.Group實例分配給每個線程組(查看后面的線程組部分)

          除了將單獨的類標記@State,也可以將你自己的benchmark類使用@State標記。上面所有的規(guī)則對這種情況也適用。

          狀態(tài)設(shè)置和清理

          與JUnit測試類似,使用@Setup@TearDown注解標記狀態(tài)類的方法(這些方法在JMH文檔中稱為_fixtures_)。setup/teardown方法的數(shù)量是任意的。這些方法不會影響測試時間(但是Level.Invocation可能影響測量精度)。

          @Setup/@TearDown注解使用Level參數(shù)來指定何時調(diào)用fixture:

          名稱描述
          Level.Trial默認level。全部benchmark運行(一組迭代)之前/之后
          Level.Iteration一次迭代之前/之后(一組調(diào)用)
          Level.Invocation每個方法調(diào)用之前/之后(不推薦使用,除非你清楚這樣做的目的)

          冗余代碼

          冗余代碼消除是microbenchmark中眾所周知的問題。通常的解決方法是以某種方式使用計算結(jié)果。JMH本身不會實施對冗余代碼的消除。但是如果你想消除冗余代碼——要做到測試程序返回值不為void永遠返回你的計算結(jié)果。JMH將完成剩余的工作。

          如果測試程序需要返回多個值,將所有這些返回值使用省時操作結(jié)合起來(省時是指相對于獲取到所有結(jié)果所做操作的開銷),或者使用BlackHole作為方法參數(shù),將所有的結(jié)果放入其中(注意某些情況下BlockHole.consume可能比手動將結(jié)果組合起來開銷更大)。BlackHole是一個thread-scoped類:

          @GenerateMicroBenchmark
          public void testSomething( BlackHole bh )
          {
              bh.consume( Math.sin( state_field ));
              bh.consume( Math.cos( state_field ));
          }
          

          常量處理

          如果計算結(jié)果是可預(yù)見的并且不依賴于狀態(tài)對象,它可能被JIT優(yōu)化。因此,最好總是從狀態(tài)對象讀取測試的輸入并且返回計算的結(jié)果。這條規(guī)則大體上用于單個返回值的情形。使用BlackHole對象JVM更難優(yōu)化它(但不是不可能被優(yōu)化)。下面測試的所有方法都不會被優(yōu)化:

          private double x = Math.PI;
          
          @GenerateMicroBenchmark
          public void bhNotQuiteRight( BlackHole bh )
          {
              bh.consume( Math.sin( Math.PI ));
              bh.consume( Math.cos( Math.PI ));
          }
          
          @GenerateMicroBenchmark
          public void bhRight( BlackHole bh )
          {
              bh.consume( Math.sin( x ));
              bh.consume( Math.cos( x ));
          }
          

          返回單個值的情形更加復(fù)雜。下面的測試不會被優(yōu)化,但是如果使用Math.log替換Math.sin,那么testWrong方法將被常量值替換。

          private double x = Math.PI;
          
          @GenerateMicroBenchmark
          public double testWrong()
          {
              return Math.sin( Math.PI );
          }
          
          @GenerateMicroBenchmark
          public double testRight()
          {
              return Math.sin( x );
          }
          

          因此,為使測試更可靠要嚴格遵守以下規(guī)則:永遠從狀態(tài)對象讀取測試輸入并返回計算的結(jié)果

          循環(huán)

          不要在測試中使用循環(huán)。JIT非常聰明,在循環(huán)中經(jīng)常出現(xiàn)不可預(yù)料的處理。要測試真實的計算,讓JMH處理剩余的部分。

          在非統(tǒng)一開銷操作情況下(比如測試處理列表的時間,這個列表在每個測試后有所增加),你可能使用@BenchmarkMode(Mode.SingleShotTime)@Measurement(batchSize = N)。但是不允許你自己實現(xiàn)測試的循環(huán)。

          分支

          默認JMH為每個試驗(迭代集合)fork一個新的java進程。這樣可以防止前面收集的“資料”——其他被加載類以及它們執(zhí)行的信息對當(dāng)前測試的影響。比如,實現(xiàn)了相同接口的兩個類,測試它們的性能,那么第一個實現(xiàn)(目標測試類)可能比第二個快,因為JIT發(fā)現(xiàn)第二個實現(xiàn)類后就把第一個實現(xiàn)的直接方法調(diào)用替換為接口方法調(diào)用。

          因此,不要把forks設(shè)為0除非你清楚這樣做的目的

          極少數(shù)情況下需要指定JVM分支數(shù)量時,使用@Fork對方法注解,就可以設(shè)置分支數(shù)量,預(yù)熱(warmup)迭代數(shù)量和JVM分支的其他參數(shù)。

          可能通過JMH API調(diào)用來指定JVM分支參數(shù)也有優(yōu)勢——可以使用一些JVM
          -XX:參數(shù),通過JMH API訪問不到它。這樣就可以根據(jù)你的代碼自動選擇最佳的JVM設(shè)置(new Runner(opt).run()以簡便的形式返回了所有的測試結(jié)果)。

          編譯器提示

          可以為JIT提供關(guān)于如何使用測試程序中任何方法的提示。“任何方法”是指任何的方法——不僅僅是@GenerateMicroBenchmark注解的方法。使用@CompilerControl模式(還有更多模式,但是我不確定它們的有用程度):

          名稱描述
          CompilerControl.Mode.DONT_INLINE該方法不能被內(nèi)嵌。用于測量方法調(diào)用開銷和評估是否該增加JVM的inline閾值
          CompilerControl.Mode.INLINE要求編譯器內(nèi)嵌該方法。通常與“Mode.DONT_INLINE“聯(lián)合使用,檢查內(nèi)嵌的利弊。
          CompilerControl.Mode.EXCLUDE不編譯該方法——解釋它。在該JIT有多好的圣戰(zhàn)中作為有用的參數(shù):)

          注解控制測試

          通過注解指定JMH參數(shù)。這些注解用在類或者方法上。方法注解總是優(yōu)先于類的注解。

          名稱描述
          @Fork需要運行的試驗(迭代集合)數(shù)量。每個試驗運行在單獨的JVM進程中。也可以指定(額外的)JVM參數(shù)。
          @Measurement提供真正的測試階段參數(shù)。指定迭代的次數(shù),每次迭代的運行時間和每次迭代測試調(diào)用的數(shù)量(通常使用@BenchmarkMode(Mode.SingleShotTime)測試一組操作的開銷——而不使用循環(huán))
          @Warmup與@Measurement相同,但是用于預(yù)熱階段
          @Threads該測試使用的線程數(shù)。默認是Runtime.getRuntime().availableProcessors()

          CPU消耗

          有時測試消耗一定CPU周期。通過靜態(tài)的BlackHole.consumeCPU(tokens)方法來實現(xiàn)。Token是一些CPU指令。這樣編寫方法代碼就可以達到運行時間依賴于該參數(shù)的目的(不被任何JIT/CPU優(yōu)化)。

          多參數(shù)的測試運行

          很多情況下測試代碼包含多個參數(shù)集合。幸運的是,要測試不同參數(shù)集合時JMH不會要求寫多個測試方法。或者準確來說,測試參數(shù)是基本類型,基本包裝類型或者String時,JMH提供了解決方法。

          程序需要完成:

          1. 定義@State對象
          2. 在其中定義所有的參數(shù)字段
          3. 每個字段都使用@Param注解

          @Param注解使用String數(shù)組作為參數(shù)。這些字符串在任何@Setup方法被調(diào)用前轉(zhuǎn)換為字段類型。然而,JMH文檔中聲稱這些字段值在@Setup方法中不能被訪問。

          JMH使用所有@Param字段的輸出結(jié)果。因此,如果第一個字段有2個參數(shù),第二個字段有5個參數(shù),測試將運行2 * 5 * Forks次。

          線程組——非統(tǒng)一的多線程

          我們已經(jīng)提到@State(Scope.Benchmark)用來測試多線程訪問狀態(tài)對象的情形。并發(fā)程度通過用來測試的線程數(shù)量設(shè)置。

          可能也需要定義對狀態(tài)對象非統(tǒng)一訪問的情況——比如測試“讀取——寫入”場景時,讀線程數(shù)通常高于寫線程數(shù)量。JMH使用線程組來應(yīng)對這種情形。

          為設(shè)置測試組,需要:

          1. 使用@Group(name)注解標記所有的測試方法,為同一個組中的所有測試設(shè)置相同的名稱(否則這些測試將獨立運行——沒有任何警告提示!)
          2. 使用@GroupThreads(threadsNumber)注解標記每個測試,指定運行給定方法的線程數(shù)量。

          JMH將啟動給定組的所有@GroupThreads,并發(fā)運行相同實驗中同一組的所有測試。組和每個方法的結(jié)果將單獨給出。

          多線程——偽共享字段訪問

          你可能知道這樣一個事實,大多數(shù)現(xiàn)代x86 CPU有64字節(jié)的cache line(緩存行)。CPU緩存提高了數(shù)據(jù)讀取速率,但同時,如果你需要從多個線程同時讀寫兩個鄰近的字段,也會產(chǎn)生性能瓶頸。這種情況稱為“偽共享”——字段似乎是獨立訪問的,但是實際上它們在硬件層面的相互競爭。

          這個問題通常的解決方案是兩邊都增加至少128字節(jié)的虛擬數(shù)據(jù)。因為JVM可以將類的字段重排序,在相同的類內(nèi)部增加可能不能正確運行。

          更加健壯的方法是使用類層次——JVM通常將屬于同一個類的字段放在一起。比如,定義類A有一個只讀字段,類B繼承類A且定義16個long字段,類C繼承類B定義可寫字段,最后類D繼承類C定義另一個16個long字段——這就防止了被分配在下一個內(nèi)存中對象的寫變量競爭。

          以防讀寫的字段類型相同,也可以使用兩個數(shù)據(jù)位置相互距離很遠的稀疏數(shù)組。在前面的情況中不要使用數(shù)組——它們是對象特定類型,僅需要增加4或8字節(jié)(取決于JVM設(shè)置)。

          這個問題的另一種解決方法是如果你已經(jīng)用到了Java 8:在可寫字段上使用@sun.misc.Contended以及-XX:-RestrictContended的JVM設(shè)置。更多細節(jié),參見Aleksey Shipilev的說明

          JMH是如何解決競爭字段訪問的呢?它在兩邊都增加了@State對象,但是這并不能在單一對象內(nèi)部對個別的字段增加——需要自己來處理。

          總結(jié)

          • JMH用于各種類型的microbenchmark——每個測試從納秒到毫秒。它關(guān)注所有可測量的邏輯,測試人員只需編寫測試方法的任務(wù)代碼。JMH也包含對所有類型多線程測試的內(nèi)在支持——統(tǒng)一(所有線程運行相同代碼)和非統(tǒng)一(線程分組,每個組運行自己的代碼)。
          • 如果僅僅一條規(guī)則需要記住的話,那就是——永遠從@State對象讀取測試輸入并返回計算的結(jié)果(無論結(jié)果是明確的還是通過
            BlackHole對象返回)

          ?

          posted on 2016-08-01 17:12 ideame 閱讀(3264) 評論(0)  編輯  收藏


          只有注冊用戶登錄后才能發(fā)表評論。


          網(wǎng)站導(dǎo)航:
           
          主站蜘蛛池模板: 鄂托克旗| 泗水县| 长海县| 清远市| 北碚区| 宜阳县| 三明市| 奉节县| 南华县| 吉林省| 大荔县| 呈贡县| 房产| 定州市| 宁远县| 垫江县| 沙洋县| 昭通市| 鄂州市| 东安县| 株洲县| 凤城市| 巴塘县| 武穴市| 乐陵市| 三门县| 宁陕县| 陆河县| 尉氏县| 阿荣旗| 建始县| 囊谦县| 佛冈县| 广西| 崇州市| 油尖旺区| 三门峡市| 辽宁省| 莱州市| 贵定县| 海南省|