目次
1 はじめに
ソフトウェア開発では、正しいコードを書くことに加えて、効率的なコードを書く必要もあります。これは、主に 2 つの理由から、同時プログラミングではさらに重要です。まず、一部の並列プログラムはシステムのパフォーマンスを向上させることを目的として逐次プログラムから変換されるため、当然のことながら 2 つのアルゴリズムのパフォーマンスを比較する方法が必要になります。次に、ビジネス上の理由でマルチスレッドを導入すると、スレッドの同時実行制御によりパフォーマンスの損失が発生する可能性があるため、損失の割合が許容できるかどうかを評価する必要があります。パフォーマンス評価の理由が何であれ、定量的な指標は常に必要です。ほとんどの場合、誰が速くて誰が遅いかを単純に答えるだけでは十分ではありません。プログラムのパフォーマンスを定量化するにはどうすればよいでしょうか? これが、このセクションで紹介する Java マイクロベンチマーク フレームワーク JMH です。
2. 従来のパフォーマンステスト
従来のパフォーマンステストでは、メソッドの前後にタイムスタンプを出力し、その時間差で実行時間を判断するのが一般的でした。
public static void dealHelloWorld() throws InterruptedException {
// 这里模拟该方法执行
Thread.sleep(1000);
}
public static void main(String[] args) throws InterruptedException {
long start0 = System.currentTimeMillis();
dealHelloWorld();
long end0 = System.currentTimeMillis();
System.out.println("执行耗时:" + (end0-start0) + "ms");
}
結果:
ただし、コードの量が多くて複雑な場合は、通常、より多くのタイムスタンプを出力し、セグメントで計算を実行する必要があります。このような:
この場合、コードの可読性を高めるために、多くのコンピューティング タイム コードがビジネス コードに組み込まれる一方で、グループの最適化されていないパフォーマンス データが統計計算に参加します。そうすると現時点ではJMHが必要になります。
3.JMHとは
JMH (Java Microbenchmark Harness) は、Java コードのパフォーマンスを正確かつ確実に測定および評価するために使用される、Java 言語用のマイクロベンチマーク テスト フレームワークです。これは、Java アプリケーションのパフォーマンス テストとベンチマークに特化して OpenJDK チームによって開発されました。複数のメソッドのパフォーマンスは、JMH によって定量的に分析できます。たとえば、関数の実行にかかる時間を知りたい場合、またはアルゴリズムの実装が多数ある場合、最高のパフォーマンスを発揮するものを選択する必要があります。
JMH公式サイトアドレス:OpenJDK:jmh
Github アドレス: https://github.com/openjdk/jmh/tags
4、こんにちはJMH
まずは試してみましょう。JMH テストの使用は非常に簡単です。Junit 単体テストの手順を考えることができます。
- junt 関連の依存関係を追加する
- テストクラス @SpringbootTest を宣言します。Mock を使用する場合は、Mock の初期設定を宣言する必要があります
- テスト スイート @JunitSuit を宣言します。テスト クラス @Test を直接記述することもできます。
同様に、JMH にもこれらの手順がありますが、依存関係が若干異なります。
4.1. Maven 関連の依存関係
<dependencies>
<!-- JMH核心代码 -->
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.35</version>
</dependency>
<!-- JMH注解相关依赖 -->
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.35</version>
</dependency>
</dependencies>
4.2. 簡単な例を書く
/**
* @author Shamee loop
* @date 2023/7/1
*/
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.SECONDS)
public class JMHTestHello01 {
/**
* @Benchmark 类似于Junit,表示被度量代码标注
*/
@Benchmark
public void dealHelloWorld() throws InterruptedException {
// 这里模拟该方法执行
Thread.sleep(1000);
}
public static void main(String[] args) throws RunnerException {
Options options = new OptionsBuilder()
.include(JMHTestHello01.class.getSimpleName())
.warmupIterations(3) // 预热的次数, 3次
.warmupTime(TimeValue.seconds(2)) // 预热的时间,2s
.forks(1) // 测试的执行线程数量
.build();
new Runner(options).run();
}
}
結果:
# ...... 这里省略部分信息,这些都是描述JDK和JMH的基础信息,基本信息等同于当下的环境以及option中的配置
# Benchmark: org.shamee.jmh.demo.JMHTestHello01.dealHelloWorld
# Run progress: 0.00% complete, ETA 00:00:56
# Fork: 1 of 1
# ...... 这里开始预热测试,我们指定了预热3次
# Warmup Iteration 1: 1.005 s/op
# Warmup Iteration 2: 1.010 s/op
# Warmup Iteration 3: 1.007 s/op
# ...... 这里迭代测试进行了5次,以及每次的时间
Iteration 1: 1.007 s/op
Iteration 2: 1.011 s/op
Iteration 3: 1.012 s/op
Iteration 4: 1.008 s/op
Iteration 5: 1.007 s/op
Result "org.shamee.jmh.demo.JMHTestHello01.dealHelloWorld":
1.009 ±(99.9%) 0.009 s/op [Average]
(min, avg, max) = (1.007, 1.009, 1.012), stdev = 0.002
CI (99.9%): [1.000, 1.018] (assumes normal distribution)
# Run complete. Total time: 00:00:59
REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.
# ...... 这里显示的汇总结果,cnt 执行了5次 score最后的结果 Error误差±0.009s units时间单位
Benchmark Mode Cnt Score Error Units
JMHTestHello01.dealHelloWorld avgt 5 1.009 ± 0.009 s/op
Process finished with exit code 0
5. 基本的な属性設定
上記のサンプルコードを見ると、JMH の使い方は複雑ではなく、コード量もそれほど多くなく、アノテーションの設定やオプションのプロパティの生成によって多くの機能が設定されていることがわかります。したがって、JMH の他の機能をより良く使用したい場合は、JMH の基本的な構成をいくつか理解する必要があります。
5.1、@BenchmarkMode
ベンチマークのスキーマ。Mode プロパティは 1 つだけあります。そして、この Mode 属性は JMH 測定のモード、つまりテスト モードを表します。
/**
* <p>Benchmark mode declares the default modes in which this benchmark
* would run. See {@link Mode} for available benchmark modes.</p>
*
* <p>This annotation may be put at {@link Benchmark} method to have effect
* on that method only, or at the enclosing class instance to have the effect
* over all {@link Benchmark} methods in the class. This annotation may be
* overridden with the runtime options.</p>
*/
@Inherited
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface BenchmarkMode {
/**
* @return Which benchmark modes to use.
* @see Mode
*/
Mode[] value();
}
モードではさまざまな方法が提供されます。
- Throughput の全体的なスループットは、1 秒以内に実行できる呼び出しの数を示します。
Throughput("thrpt", "Throughput, ops/time")
- AverageTime 呼び出しの平均時間。1 秒あたりの呼び出しに必要な時間を指します。
AverageTime("avgt", "Average time, time/op")
- SampleTime はランダムにサンプリングし、最終的に「呼び出しの 99% が xxx ミリ秒以内、呼び出しの 99.99% が xxx ミリ秒以内」などのサンプリング結果の分布を出力します。
SampleTime("sample", "Sampling time")
- SingleShotTime 上記のモードはすべてデフォルトで 1 秒の反復に設定されており、これは 1 回だけ実行されることを意味します。コールド スタート時のパフォーマンスをテストするために、ウォームアップの回数は 0 に設定されることがよくあります。
SingleShotTime("ss", "Single shot invocation time")
- すべて 上記のモードをすべて実行します。
All("all", "All benchmark modes")
5.2、@ベンチマーク
@Benchmark は @Test に似ており、どのメソッドがテストの対象となるかを JMH に伝えるために使用されます。メソッドにのみアノテーションを付けることができます。テスト プロジェクトがパッケージ化されるときと少し似ていますが、JMH は @Benchmark アノテーションが付けられたメソッドのベンチマーク メソッド コードを生成します。通常、各ベンチマーク メソッドは相互に干渉することなく、独立したプロセスで実行されます。
5.3、オプションビルダーとオプション
これはテストを構成する構成クラスです。通常、実行テスト クラス (include)、使用されるプロセスの数 (fork)、ウォームアップ反復の数 (warmupInterations) など、いくつかのパラメーターを指定する必要があります。上記のコードのように、構成がテストを開始するときに実行します。
Options options = new OptionsBuilder()
.include(JMHTestHello01.class.getSimpleName())
.warmupIterations(3) // 预热的次数, 3次
.warmupTime(TimeValue.seconds(2)) // 预热的时间,2s
.forks(1) // 测试的执行线程数量
.build();
5.4、反復 反復
反復は JMH の測定単位です。ほとんどの測定モードでは、1 回の繰り返しが 1 秒を表します。この 2 秒間、テスト対象のメソッドが継続的に呼び出され、スループットや平均時間などがサンプリングによって計算されます。
これは、OptionsBuilder またはアノテーションを使用して構成できます。
Options options = new OptionsBuilder()
.include(JMHTestHello01.class.getSimpleName())
.measurementIterations(3).build(); // 执行的次数, 3次
また
@Measurement(iterations = 3)
@Benchmark
public void dealHelloWorld() throws InterruptedException {
// 这里模拟该方法执行
Thread.sleep(1000);
}
5.5. ウォームアップ
Java仮想マシンのJITの存在により、同じメソッドでもJITコンパイル前後で時間が異なります。通常、JIT コンパイル後のメソッドのパフォーマンスのみが考慮されます。ウォームアップ テストは、最終的な統計結果としては使用されません。ウォームアップの目的は、Java 仮想マシンがテスト対象のコードを十分に最適化できるようにすることです。
同様に、予熱も OptionsBuilder を通じて構成でき、注釈も使用できます。
Options options = new OptionsBuilder()
.include(JMHTestHello01.class.getSimpleName())
.warmupIterations(3).build(); // 预热的次数, 3次
また
@Warmup(iterations = 3)
@Benchmark
public void dealHelloWorld() throws InterruptedException {
// 这里模拟该方法执行
Thread.sleep(1000);
}
5.6、状態 状態
オブジェクトのスコープは State を通じて指定でき、インスタンス化と共有操作は JMH の Scope を通じて実行されます。
@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface State {
/**
* State scope.
* @return state scope
* @see Scope
*/
Scope value();
}
- Scope.Benchmark: ベンチマーク スコープ。すべてのテスト スレッドは 1 つのインスタンスを共有し、マルチスレッド共有下でステートフル インスタンスのパフォーマンスをテストします。
- Scope.Group: 同じスレッドが同じグループ内のインスタンスを共有します
- Scope.Thread: デフォルト状態、スレッドスコープ。つまり、オブジェクトは 1 つのスレッドによってのみアクセスされます。マルチスレッド プールでテストする場合、スレッドごとにオブジェクトが生成されます。
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.SECONDS)
@State(Scope.Benchmark)
public class JMHTestHello01 {
}
6.IDEA JMHプラグイン
Junit テスト クラスのコード生成ツールと同様に、JMH にも対応するテスト コード自動生成ツール プラグインがあります。
プラグイン JMH Java Microbenchmark Harness をダウンロードしてインストールします。
インストール完了後、テストコードを生成したい場所で右クリック→生成→JMHベンチマークの生成を行うと自動生成されます。
その後、実際のニーズに応じてテストする必要があるプロパティ構成を変更するだけでよく、マウスの右ボタンで直接実行して結果を表示できます。
7. まとめ
実際のプロジェクトでは、JMHを利用することで、開発者はJavaコードのパフォーマンスを正確に測定・分析し、パフォーマンスのチューニングや最適化を行うことができます。これは、開発者がさまざまな環境でのコードのパフォーマンスをより深く理解し、パフォーマンスのボトルネックを特定し、最適化の方向性と戦略を見つけるのに役立ちます。ただし、JMH は強力ですが、使用する場合はテスト シナリオとパラメーターを慎重に選択し、テスト結果の精度と信頼性を確保するために使用される統計手法と測定指標を理解する必要があることに注意してください。