현이의 개발 이야기

제미나이로 JMH 공부하기 - 1. JMH 입문


저번 포스트에서 제미나이가 작성해준 커리큘럼대로 실제 학습을 진행해보자.

https://www.hyuni.dev/18

 

제미나이로 JMH 공부하기 - 0. 학습 플랜 짜기

작년 6월, Medium에 Java의 Stream과 반복문을 비교하는 글을 올렸다.https://medium.com/better-programming/can-streams-replace-loops-in-java-f56d4461743a Can Streams Replace Loops in Java?Code readability, performance, and limitations of St

www.hyuni.dev

 

이번 포스트는 커리큘럼의 첫 번째 모듈, JMH 입문이다.

 

이 모듈에 대한 목차는 다음과 같다.

  1. JMH란 무엇인가?
  2. JMH 구성하기
  3. 기본적인 JMH 어노테이션
  4. 예제: 첫 번째 JMH 벤치마크

JMH란 무엇인가?

JMH(Java Microbenchmark Harness)는 자바 코드의 성능을 신뢰성 있게 측정할 수 있도록 고안된 툴킷이다.

마이크로벤치마킹

마이크로벤치마킹(Microbenchmarking)은 작고, 독립적인 코드 조각이나 메서드의 성능을 측정하는데 중점을 두고 있다.

이는 다음과 같은 문제를 해결하는데 도움이 된다.

  • 특정 연산이 수행되는데 걸리는 시간 측정하기
  • 특정 크기의 입력에 대해 두 알고리즘의 성능 비교하기
  • 파라미터의 변경이 성능에 미치는 영향 파악하기

JMH의 가치

JMH는 마이크로벤치마크를 구성하는 프로세스를 간략화시키고, 흔히 발생하는 다음의 문제점들을 해결할 수 있다.

  • JIT Compilation
    • JMH는 자바 가상 머신 (JVM, Java Virtual Machine)이 warm up되는 것을 보장한다.
    • 이는 실제 측정을 하기 전 대상 코드를 여러 번 미리 실행하는 방식으로 처리한다.
    • 초기 컴파일 오버헤드를 배제하여 성능 측정 결과의 신뢰성을 높일 수 있다.
이 부분을 보고, 내가 전에 Stream과 반복문을 비교할 때 사용했던 성능 측정 방식이 JIT Compilation으로 인해 dilute된 것이 아닌가 하는 생각이 들었다.

당시에 코드의 실행 전과 후에 System.currentTimeMillis()를 이용해 시간을 구하는 방식으로 코드의 성능을 측정했는데, 이렇게 하면 프로그램이 실행되고 JVM이 warm up되는 도중에 작업이 완료되어버려 제대로 된 성능 측정이 되지 않았을 것 같다.
  • Garbage Collection
    • JMH는 벤치마킹 도중 garbage collection의 동작 방식을 제어할 수 있는 옵션을 제공한다.
    • 이를 이용해 garbage collection으로 인한 간섭 없이 코드의 성능을 독립적으로 측정할 수 있다.
  • 벤치마크 오버헤드
    • JMH를 사용하면 마이크로벤치마킹에 필요한 boilerplate 코드를 최대한 줄일 수 있다.
    • 따라서 성능을 측정하고자 하는 코드에 집중할 수 있다.

JMH 구성하기

Maven을 이용해 구성하기

dependency 추가

다음의 dependency들을 pom.xml 파일에 추가한다.

<dependencies>
    <dependency>
        <groupId>org.openjdk.jmh</groupId>
        <artifactId>jmh-core</artifactId>
        <version>1.36</version>
    </dependency>
    <dependency>
        <groupId>org.openjdk.jmh</groupId>
        <artifactId>jmh-generator-annprocess</artifactId>
        <version>1.36</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

plugin 추가

pom.xml에 다음을 추가한다.

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-shade-plugin</artifactId>
            <version>3.2.0</version>
            <executions>
                <execution>
                    <phase>package</phase>
                    <goals>
                        <goal>shade</goal>
                    </goals>
                    <configuration>
                        <finalName>benchmarks</finalName>
                        <transformers>
                            <transformer
                                    implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                <mainClass>org.openjdk.jmh.Main</mainClass>
                            </transformer>
                        </transformers>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

 

pom.xml 전체 예시 ↓

더보기
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>learning-jmh</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-core</artifactId>
            <version>1.36</version>
        </dependency>
        <dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-generator-annprocess</artifactId>
            <version>1.36</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.2.0</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <finalName>benchmarks</finalName>
                            <transformers>
                                <transformer
                                        implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>org.openjdk.jmh.Main</mainClass>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

실행

  1. 컴파일: mvn package
  2. 벤치마크 실행: java -jar target/benchmarks.jar [테스트하려는 클래스]

기본적인 JMH 어노테이션

@Benchmark

성능을 측정할 메서드를 표시한다.

@BenchmarkMode

측정 방식을 명시한다.

Mode.Throughput

  • 특정 시간 단위에 수행 된 연산의 횟수를 측정 (op/s)
  • 주어진 시간 안에 얼마나 많은 연산을 할 수 있는지를 알아볼 때 유용
  • 예시) 데이터 프로세싱 파이프라인의 성능 측정

Mode.AverageTime

  • 각 연산 당 소요되는 시간을 측정 (s/op)
  • 개별 연산에 걸리는 시간을 이해할 때 유용
  • 예시) 웹 서버의 평균적인 응답 속도 측정

Mode.SampleTime

  • 개별 연산의 소요 시간 분포를 측정
  • 코드의 성능의 랜덤성을 파악할 때 유용
  • 예시) 쿼리별로 속도가 다를 수 있는 데이터베이스 쿼리 성능 측정

Mode.SingleShotTime

  • 연산의 1회 실행 시간 측정
  • 처음 실행했을 때의 시간이 중요한 cold-start 시나리오에서 주로 사용
  • 예시) 자바 어플리케이션의 시작 시간 측정

@OutputTimeUnit

분석 리포트 결과에 사용할 시간 단위를 명시할 수 있다.

@State

벤치마크의 상태. 라이프사이클과 데이터를 공유하는 방식을 제어할 수 있다.

예시: 첫 번째 JMH 벤치마크

JMH를 이용해 벤치마크를 하는 예시를 작성해보자.

 

문자열을 이어 붙이기 위해 사용하는 다음의 세 가지 방식을 비교해 볼 것이다.

  1. StringBuilder 사용하기
  2. StringBuffer 사용하기
  3. + 연산자 사용하기

문자열을 반복적으로 이어 붙일 때 StringBuilder를 사용하는 것은 널리 알려진 best practice이다.

 

실제로 성능을 측정하여 얼마만큼의 차이가 발생하는지 살펴보자.

import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.AverageTime) 
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread) 
public class StringConcatBenchmark {

    @Benchmark
    public String stringBuilderConcat() {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 100; i++) {
            sb.append("a");
        }
        return sb.toString();
    }

    @Benchmark
    public String stringBufferConcat() {
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < 100; i++) {
            sb.append("a");
        }
        return sb.toString();
    }

    @Benchmark
    public String plusOperatorConcat() {
        String s = "";
        for (int i = 0; i < 100; i++) {
            s += "a";
        }
        return s;
    }
}

 

이를 실행하면 다음과 같은 결과가 나온다.

Benchmark Mode Cnt Score Error Units
StringConcatBenchmark.plusOperatorConcat avgt 25 597.438 ± 2.934 ns/op
StringConcatBenchmark.stringBufferConcat avgt 25 82.658 ± 1.976 ns/op
StringConcatBenchmark.stringBuilderConcat  avgt 25 80.872 ± 2.138 ns/op

 

실행 결과 전문 ↓

더보기
# JMH version: 1.36
# VM version: JDK 21.0.1, OpenJDK 64-Bit Server VM, 21.0.1+12-29
# VM invoker: C:\Users\PC\.jdks\openjdk-21.0.1\bin\java.exe
# VM options: <none>
# Blackhole mode: compiler (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
# Warmup: 5 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: benchmark.StringConcatBenchmark.plusOperatorConcat

# Run progress: 0.00% complete, ETA 00:25:00
# Fork: 1 of 5
# Warmup Iteration   1: 656.176 ns/op
# Warmup Iteration   2: 600.166 ns/op
# Warmup Iteration   3: 605.244 ns/op
# Warmup Iteration   4: 613.561 ns/op
# Warmup Iteration   5: 604.866 ns/op
Iteration   1: 607.789 ns/op
Iteration   2: 605.505 ns/op
Iteration   3: 601.387 ns/op
Iteration   4: 602.623 ns/op
Iteration   5: 604.010 ns/op

# Run progress: 6.67% complete, ETA 00:23:24
# Fork: 2 of 5
# Warmup Iteration   1: 612.324 ns/op
# Warmup Iteration   2: 594.178 ns/op
# Warmup Iteration   3: 596.043 ns/op
# Warmup Iteration   4: 594.207 ns/op
# Warmup Iteration   5: 594.994 ns/op
Iteration   1: 597.702 ns/op
Iteration   2: 594.580 ns/op
Iteration   3: 594.700 ns/op
Iteration   4: 596.080 ns/op
Iteration   5: 594.819 ns/op

# Run progress: 13.33% complete, ETA 00:21:43
# Fork: 3 of 5
# Warmup Iteration   1: 631.640 ns/op
# Warmup Iteration   2: 592.285 ns/op
# Warmup Iteration   3: 593.925 ns/op
# Warmup Iteration   4: 595.769 ns/op
# Warmup Iteration   5: 593.534 ns/op
Iteration   1: 593.041 ns/op
Iteration   2: 594.010 ns/op
Iteration   3: 596.724 ns/op
Iteration   4: 594.419 ns/op
Iteration   5: 594.869 ns/op

# Run progress: 20.00% complete, ETA 00:20:03
# Fork: 4 of 5
# Warmup Iteration   1: 632.448 ns/op
# Warmup Iteration   2: 590.877 ns/op
# Warmup Iteration   3: 595.224 ns/op
# Warmup Iteration   4: 597.821 ns/op
# Warmup Iteration   5: 594.290 ns/op
Iteration   1: 594.628 ns/op
Iteration   2: 594.731 ns/op
Iteration   3: 595.364 ns/op
Iteration   4: 594.621 ns/op
Iteration   5: 598.375 ns/op

# Run progress: 26.67% complete, ETA 00:18:23
# Fork: 5 of 5
# Warmup Iteration   1: 624.502 ns/op
# Warmup Iteration   2: 592.441 ns/op
# Warmup Iteration   3: 597.638 ns/op
# Warmup Iteration   4: 597.417 ns/op
# Warmup Iteration   5: 596.176 ns/op
Iteration   1: 599.915 ns/op
Iteration   2: 596.552 ns/op
Iteration   3: 596.036 ns/op
Iteration   4: 596.286 ns/op
Iteration   5: 597.190 ns/op


Result "benchmark.StringConcatBenchmark.plusOperatorConcat":
  597.438 ±(99.9%) 2.934 ns/op [Average]
  (min, avg, max) = (593.041, 597.438, 607.789), stdev = 3.917
  CI (99.9%): [594.504, 600.372] (assumes normal distribution)


# JMH version: 1.36
# VM version: JDK 21.0.1, OpenJDK 64-Bit Server VM, 21.0.1+12-29
# VM invoker: C:\Users\PC\.jdks\openjdk-21.0.1\bin\java.exe
# VM options: <none>
# Blackhole mode: compiler (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
# Warmup: 5 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: benchmark.StringConcatBenchmark.stringBufferConcat

# Run progress: 33.33% complete, ETA 00:16:42
# Fork: 1 of 5
# Warmup Iteration   1: 85.262 ns/op
# Warmup Iteration   2: 81.639 ns/op
# Warmup Iteration   3: 83.459 ns/op
# Warmup Iteration   4: 79.012 ns/op
# Warmup Iteration   5: 81.272 ns/op
Iteration   1: 81.941 ns/op
Iteration   2: 78.524 ns/op
Iteration   3: 82.685 ns/op
Iteration   4: 79.806 ns/op
Iteration   5: 80.863 ns/op

# Run progress: 40.00% complete, ETA 00:15:02
# Fork: 2 of 5
# Warmup Iteration   1: 90.087 ns/op
# Warmup Iteration   2: 88.633 ns/op
# Warmup Iteration   3: 85.027 ns/op
# Warmup Iteration   4: 84.202 ns/op
# Warmup Iteration   5: 85.716 ns/op
Iteration   1: 85.315 ns/op
Iteration   2: 85.288 ns/op
Iteration   3: 84.483 ns/op
Iteration   4: 85.203 ns/op
Iteration   5: 84.587 ns/op

# Run progress: 46.67% complete, ETA 00:13:22
# Fork: 3 of 5
# Warmup Iteration   1: 83.975 ns/op
# Warmup Iteration   2: 85.991 ns/op
# Warmup Iteration   3: 77.368 ns/op
# Warmup Iteration   4: 82.878 ns/op
# Warmup Iteration   5: 80.736 ns/op
Iteration   1: 80.022 ns/op
Iteration   2: 84.398 ns/op
Iteration   3: 78.315 ns/op
Iteration   4: 82.121 ns/op
Iteration   5: 81.290 ns/op

# Run progress: 53.33% complete, ETA 00:11:42
# Fork: 4 of 5
# Warmup Iteration   1: 84.193 ns/op
# Warmup Iteration   2: 85.646 ns/op
# Warmup Iteration   3: 79.019 ns/op
# Warmup Iteration   4: 82.809 ns/op
# Warmup Iteration   5: 80.689 ns/op
Iteration   1: 79.168 ns/op
Iteration   2: 84.304 ns/op
Iteration   3: 78.332 ns/op
Iteration   4: 80.878 ns/op
Iteration   5: 81.412 ns/op

# Run progress: 60.00% complete, ETA 00:10:01
# Fork: 5 of 5
# Warmup Iteration   1: 90.445 ns/op
# Warmup Iteration   2: 87.759 ns/op
# Warmup Iteration   3: 84.884 ns/op
# Warmup Iteration   4: 85.708 ns/op
# Warmup Iteration   5: 84.740 ns/op
Iteration   1: 85.710 ns/op
Iteration   2: 85.631 ns/op
Iteration   3: 84.724 ns/op
Iteration   4: 85.660 ns/op
Iteration   5: 85.790 ns/op


Result "benchmark.StringConcatBenchmark.stringBufferConcat":
  82.658 ±(99.9%) 1.976 ns/op [Average]
  (min, avg, max) = (78.315, 82.658, 85.790), stdev = 2.638
  CI (99.9%): [80.682, 84.634] (assumes normal distribution)


# JMH version: 1.36
# VM version: JDK 21.0.1, OpenJDK 64-Bit Server VM, 21.0.1+12-29
# VM invoker: C:\Users\PC\.jdks\openjdk-21.0.1\bin\java.exe
# VM options: <none>
# Blackhole mode: compiler (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
# Warmup: 5 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: benchmark.StringConcatBenchmark.stringBuilderConcat

# Run progress: 66.67% complete, ETA 00:08:21
# Fork: 1 of 5
# Warmup Iteration   1: 87.285 ns/op
# Warmup Iteration   2: 88.340 ns/op
# Warmup Iteration   3: 75.134 ns/op
# Warmup Iteration   4: 80.172 ns/op
# Warmup Iteration   5: 77.527 ns/op
Iteration   1: 75.676 ns/op
Iteration   2: 81.223 ns/op
Iteration   3: 75.642 ns/op
Iteration   4: 78.821 ns/op
Iteration   5: 78.159 ns/op

# Run progress: 73.33% complete, ETA 00:06:41
# Fork: 2 of 5
# Warmup Iteration   1: 88.340 ns/op
# Warmup Iteration   2: 83.463 ns/op
# Warmup Iteration   3: 80.816 ns/op
# Warmup Iteration   4: 76.247 ns/op
# Warmup Iteration   5: 77.632 ns/op
Iteration   1: 79.221 ns/op
Iteration   2: 76.321 ns/op
Iteration   3: 81.314 ns/op
Iteration   4: 76.005 ns/op
Iteration   5: 76.436 ns/op

# Run progress: 80.00% complete, ETA 00:05:00
# Fork: 3 of 5
# Warmup Iteration   1: 94.094 ns/op
# Warmup Iteration   2: 91.036 ns/op
# Warmup Iteration   3: 82.172 ns/op
# Warmup Iteration   4: 83.052 ns/op
# Warmup Iteration   5: 83.093 ns/op
Iteration   1: 82.076 ns/op
Iteration   2: 83.209 ns/op
Iteration   3: 83.495 ns/op
Iteration   4: 82.852 ns/op
Iteration   5: 83.021 ns/op

# Run progress: 86.67% complete, ETA 00:03:20
# Fork: 4 of 5
# Warmup Iteration   1: 93.181 ns/op
# Warmup Iteration   2: 90.700 ns/op
# Warmup Iteration   3: 83.323 ns/op
# Warmup Iteration   4: 83.163 ns/op
# Warmup Iteration   5: 82.308 ns/op
Iteration   1: 82.681 ns/op
Iteration   2: 83.134 ns/op
Iteration   3: 82.378 ns/op
Iteration   4: 83.014 ns/op
Iteration   5: 82.888 ns/op

# Run progress: 93.33% complete, ETA 00:01:40
# Fork: 5 of 5
# Warmup Iteration   1: 93.726 ns/op
# Warmup Iteration   2: 90.703 ns/op
# Warmup Iteration   3: 83.531 ns/op
# Warmup Iteration   4: 83.432 ns/op
# Warmup Iteration   5: 81.823 ns/op
Iteration   1: 83.261 ns/op
Iteration   2: 83.141 ns/op
Iteration   3: 82.180 ns/op
Iteration   4: 82.258 ns/op
Iteration   5: 83.407 ns/op

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.

NOTE: Current JVM experimentally supports Compiler Blackholes, and they are in use. Please exercise
extra caution when trusting the results, look into the generated code to check the benchmark still
works, and factor in a small probability of new VM bugs. Additionally, while comparisons between
different JVMs are already problematic, the performance difference caused by different Blackhole
modes can be very significant. Please make sure you use the consistent Blackhole mode for comparisons.

Benchmark                                  Mode  Cnt    Score   Error  Units
StringConcatBenchmark.plusOperatorConcat   avgt   25  597.438 ± 2.934  ns/op
StringConcatBenchmark.stringBufferConcat   avgt   25   82.658 ± 1.976  ns/op
StringConcatBenchmark.stringBuilderConcat  avgt   25   80.872 ± 2.138  ns/op

 

코딩 테스트를 준비하고 있다면? 더 좋은 코드를 작성하고 싶다면?
79개 문제 풀이, 코딩전문역량인증시험(PCCP) 대비까지!

합격에 한 걸음 더 가까워지는 실전형 코딩 테스트 문제 풀이 가이드
취업과 이직을 위한 프로그래머스 코딩 테스트 문제 풀이 전략 : 자바편