JMH 이란?
JMH(Java Microbenchmark Harness)는 openjdk에서 만든 라이브러리입니다
JMH는 JVM을 대상으로 한 자바 및 다른 언어로 작성된 nano/micro/milli/macro를 구축, 실행 및 분석하기 위한 java harness입니다
또한, JMH은 어노테이션 기반 방식을 지원하기 때문에 간단하고 안정적으로 벤치 마크 구현이 가능합니다.
JMH이 안정적으로 벤치 구현이 가능한 이유는 JVM으로 실행되는 프로그램은 핫스팟이 바이트코드를 최적화 하는데 필요한 준비 시간,
가비지 컬렉터로 인한 오버헤드 등과 같은 여러 요소를 고려해야하기 때문입니다
gradle에서 JMH를 사용하기
JMH 벤치마크를 실행하는 권장 방법은 Maven을 사용하여 jar 파일에 의존하는 독립 실행형 프로젝트를 설정하는 것입니다.
이렇게 하면 벤치마크를 올바르게 초기화하고 신뢰할 수 있는 결과를 얻을 수 있기 때문입니다
그래서 JMH는 Maven이외의 빌드 시스템에 대한 빌드 스크립트를 제공하지 않습니다
하지만 커뮤니티에서 지원하는 바인딩이 있습니다
jmh-gradle-plugin를 이용하면 gradle 빌드 시스템에서도 할 수 있습니다
그러면 gradle 프로젝트에서 jmh 사용하기 위해 몇 가지 작업을 진행해보겠습니다
build.gradle 샘플
plugins {
id "me.champeau.jmh" version "0.6.6"
}
plugins에 "me.champeau.jmh"를 추가합니다
0.6.0 이전 버전의 플러그인은 me.champau.gradle.jmh 플러그인 ID를 사용했습니다
또한 0.6버전 이상 부터는 gradle 6.8+이 필요합니다
벤치마크 소스 파일은 src/jmh에서 찾을 수 있기 때문에 변경합니다
src/jmh
|- java : java sources for benchmarks
|- resources : resources for benchmarks
build.gradle에 org.openjdk.jmh:jmh-core
와
자바 아카이브 (jar) 파일을 만드는 데 도움을 주는 org.openjdk.jmh:jmh-generator-annprocess
의존성을 추가합니다
dependencies {
...
implementation 'org.openjdk.jmh:jmh-core:1.35'
jmhAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.35'
}
jmh 옵션을 추가하고 싶다면 [참고]를 확인해주세요!
이렇게 설정하면 프로젝트에 따라 다음과 같은 gradle Tasks가 추가됩니다
JMH Gradle Tasks
jmhClasses | raw benchmark code 컴파일 |
jmhRunBytecodeGenerator | raw benchmark code에 대해 바이트 코드 제너레이터를 실행하여 실제 벤치마크를 생성 |
jmhCompileGeneratedClasses | 생성된 벤치마크 컴파일 |
jmhJar | JMH 런타임과 컴파일된 벤치마크 클래스를 포함하는 JMH jar를 빌드 |
jmh | 벤치마크를 실행 |
jmh는 메인 테스크이며 다른 테스크에 의존하기 때문에 일반적으로 jmh 테스크를 수행하는 것만으로도 충분하다고 나와있습니다
gradle jmh
원하는 Java 코드에 JMH를 사용하여 벤치마킹 하기
JMH 테스트를 이해하고 작성하려면 JMH 샘플을 통해 작업하는 것이 유용할 수 있다고 나와있습니다
하지만 이번 글은 JMH 라이브러리를 이용해 스트림 성능 측정으로
모던 자바 인 액션에서 작성한 스트림 성능 측정으로 진행해보겠습니다
@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime) // 벤치마크 대상 메소드를 실행하는 데 걸린 평균 시간 측정
@OutputTimeUnit(TimeUnit.MILLISECONDS) // 벤치마크 결과를 밀리초 단위로 출력
@Fork(value = 2, jvmArgs = {"-Xms4G", "-Xmx4G"}) // 4Gb의 힙 공간을 제공한 환경에서 두 번 벤치마크를 수행해 결과의 신뢰성 확보
public class ParallelStreamBenchmark {
private static final long N = 10_000_000L;
@Benchmark // 벤치마크 대상 메소드
public long sequentialSum() {
return Stream.iterate(1L, i -> i + 1)
.limit(N)
.reduce(0L, Long::sum);
}
@TearDown(Level.Invocation) // 매 번 벤치마크를 실행한 다음에는 가비지 컬렉터 동작 시도
public void tearDown() {
System.gc();
}
}
메소드
벤치마크가 가능한 가비지 컬렉터의 영향을 받지 않도록 힙의 크기를 충분하게 설정한 것 뿐만 아니라
벤치마크가 끝날 때마다 가비지 컬텍터가 동작했습니다
모던인 자바 인 액션 책 코드와 다른 점은 바로 @State입니다
처음에 발생한 에러는 아래와 같습니다
@TearDown annotation is placed within a class not having @State annotation.
This is prohibited because it would have no effect.
말 그대로 @TearDown은 @State 이 없는 클래스 내에 배치 되어 발생한 에러로 @State이 필수라는 것을 알 수 있다
org.openjdk.jmh.annotations 패키지에 State.java를 확인해봤다
state object는 벤치마크가 동작하고 있는 상태를 자연스럽게 캡슐화한다
state object는 보통 벤치마크 메소드에 인수로 주입되는데 JMH는 해당 인스턴스화와 공유를 처리한다
state object는 다른 State 객체의 Setup메소드와 TearDown 메소드에 주입되어 스테이징된 초기화를 가져올 수 있다
JMH는 상태 객체를 재사용할 수 있는 다양한 범위를 상태 객체 범위를 통해 제공한다
상태 객체 범위는 작업 스레드 간에 공유되는 범위를 말하며 @State 어노테이션의 매개 변수에 저장된다
Scope 클래스(상태 객체 범위)에는 다음과 같은 범위 상수가 포함되어 있다
Thread | 벤치마크를 실행하는 각 스레드는 state object의 자체 인스턴스를 만듭니다. |
Group | 벤치마크를 실행하는 각 스레드 그룹은 state object의 자체 인스턴스를 만듭니다. |
Benchmark | 벤치마크를 실행하는 모든 스레드는 동일한 state object를 공유합니다. |
실행 해보기
모던 자바 인 액션을 보면 코드를 실행할 때 JMH 명령은 핫스팟이 코드를 최적화할 수 있도록 여러번 실행하여
벤치마크를 준비한 다음 여러번을 더 실행해 최종 결과를 계산한다고 한다
JMH의 특정 어노테이션이나 -w, -i 플래그를 명령행에 추가해서 실행 횟수(기본 동작 횟수)를 조절할 수 있다고 나와있다
-i는 iterations이고 -w은 warmup이다
jmh-gradle-plugin 에서 jmh의 구성 옵션은 아래와 같다
jmh {
includes = ['some regular expression'] // include pattern (regular expression) for benchmarks to be executed
excludes = ['some regular expression'] // exclude pattern (regular expression) for benchmarks to be executed
iterations = 10 // Number of measurement iterations to do.
benchmarkMode = ['thrpt','ss'] // Benchmark mode. Available modes are: [Throughput/thrpt, AverageTime/avgt, SampleTime/sample, SingleShotTime/ss, All/all]
batchSize = 1 // Batch size: number of benchmark method calls per operation. (some benchmark modes can ignore this setting)
fork = 2 // How many times to forks a single benchmark. Use 0 to disable forking altogether
failOnError = false // Should JMH fail immediately if any benchmark had experienced the unrecoverable error?
forceGC = false // Should JMH force GC between iterations?
jvm = 'myjvm' // Custom JVM to use when forking.
jvmArgs = ['Custom JVM args to use when forking.']
jvmArgsAppend = ['Custom JVM args to use when forking (append these)']
jvmArgsPrepend =[ 'Custom JVM args to use when forking (prepend these)']
humanOutputFile = project.file("${project.buildDir}/results/jmh/human.txt") // human-readable output file
resultsFile = project.file("${project.buildDir}/results/jmh/results.txt") // results file
operationsPerInvocation = 10 // Operations per invocation.
benchmarkParameters = [:] // Benchmark parameters.
profilers = [] // Use profilers to collect additional data. Supported profilers: [cl, comp, gc, stack, perf, perfnorm, perfasm, xperf, xperfasm, hs_cl, hs_comp, hs_gc, hs_rt, hs_thr, async]
timeOnIteration = '1s' // Time to spend at each measurement iteration.
resultFormat = 'CSV' // Result format type (one of CSV, JSON, NONE, SCSV, TEXT)
synchronizeIterations = false // Synchronize iterations?
threads = 4 // Number of worker threads to run with.
threadGroups = [2,3,4] //Override thread group distribution for asymmetric benchmarks.
timeout = '1s' // Timeout for benchmark iteration.
timeUnit = 'ms' // Output time unit. Available time units are: [m, s, ms, us, ns].
verbosity = 'NORMAL' // Verbosity mode. Available modes are: [SILENT, NORMAL, EXTRA]
warmup = '1s' // Time to spend at each warmup iteration.
warmupBatchSize = 10 // Warmup batch size: number of benchmark method calls per operation.
warmupForks = 0 // How many warmup forks to make for a single benchmark. 0 to disable warmup forks.
warmupIterations = 1 // Number of warmup iterations to do.
warmupMode = 'INDI' // Warmup mode for warming up selected benchmarks. Warmup modes are: [INDI, BULK, BULK_INDI].
warmupBenchmarks = ['.*Warmup'] // Warmup benchmarks to include in the run in addition to already selected. JMH will not measure these benchmarks, but only use them for the warmup.
zip64 = true // Use ZIP64 format for bigger archives
jmhVersion = '1.29' // Specifies JMH version
includeTests = true // Allows to include test sources into generate JMH jar, i.e. use it when benchmarks depend on the test classes.
duplicateClassesStrategy = DuplicatesStrategy.FAIL // Strategy to apply when encountring duplicate classes during creation of the fat jar (i.e. while executing jmhJar task)
}
참고
https://openjdk.org/projects/code-tools/jmh/
OpenJDK: jmh
Code Tools: jmh JMH is a Java harness for building, running, and analysing nano/micro/milli/macro benchmarks written in Java and other languages targetting the JVM. Links
openjdk.org
https://github.com/melix/jmh-gradle-plugin
GitHub - melix/jmh-gradle-plugin: Integrates the JMH benchmarking framework with Gradle
Integrates the JMH benchmarking framework with Gradle - GitHub - melix/jmh-gradle-plugin: Integrates the JMH benchmarking framework with Gradle
github.com
http://javadox.com/org.openjdk.jmh/jmh-core/1.7/org/openjdk/jmh/annotations/State.html
State (JMH Core 1.7 API) - Javadoc Extreme
New Blog Post! Astyanax, the Cassandra Java library New blog post: Getting started with Astyanax, the open source Cassandra java library and connect your application to one of the most important NoSQL database. Read Blog Post
javadox.com
'IT > 기록' 카테고리의 다른 글
[Flutter] Flutter 앱에 Firebase 파이어베이스 설정하기 (0) | 2022.08.08 |
---|---|
21년 12월 JAVA 보안 취약점 이슈와 API 보안에 대해 학습 (0) | 2022.08.04 |
사용자 입력이 필요한 JUnit Test (0) | 2022.07.19 |
Spring Boot DevTools 사용 (0) | 2022.07.04 |
[Error] Set the spring.mongodb.embedded.version property or define your own MongodConfig bean to use embedded MongoDB (0) | 2022.06.29 |