IT/JAVA

JMH (JMH 라이브러리를 활용한 Java 코드 성능테스트)

iamhyeon 2025. 4. 11. 20:29

JMH 란?

- Java Microbenchmark Harness

- Java 공식 성능 측정 도구
- OpenJDK 팀에서 만든 라이브러리로, 마이크로 벤치마크를 정밀하게 측정할 수 있게 도와준다.

특징
- JIT(Just-In-Time) 최적화 고려
- 워밍업 시간, 반복 횟수 조정 가능
- 평균/최댓값/표준편차 등 정밀 통계 제공
- GC와 캐시 등 JVM 영향 최소화


 

( 실행 환경 )

Gradle 8.10.2

Java 17

VS Code

Windows 11


 

✅ Gradle 프로젝트 생성

 

더보기

< VS Code 에서 Gradle 프로젝트 생성 방법  >

VS Code 에서  단축키  Ctrl+Shift+P

Create Java Project 선택

 

Gradle 선택

 

Groovy 선택

프로젝트명 입력  


 

✅ build.gradle 설정

/*
 * This file was generated by the Gradle 'init' task.
 *
 * This generated file contains a sample Java application project to get you started.
 * For more details on building Java & JVM projects, please refer to https://docs.gradle.org/8.8/userguide/building_java_projects.html in the Gradle documentation.
 */

plugins {
    // Apply the application plugin to add support for building a CLI application in Java.
    id 'java'
    id "me.champeau.jmh" version "0.7.2"
}

repositories {
    // Use Maven Central for resolving dependencies.
    mavenCentral()
}

dependencies {
    // Use JUnit Jupiter for testing.
    testImplementation libs.junit.jupiter

    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

    // This dependency is used by the application.
    implementation libs.guava
}

// Apply a specific Java toolchain to ease working on different environments.
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

tasks.named('test') {
    // Use JUnit Platform for unit tests.
    useJUnitPlatform()
}

tasks.withType(JavaCompile).configureEach {
    options.encoding = 'UTF-8'
}

 

plugins {
    // Apply the application plugin to add support for building a CLI application in Java.
    id 'java'
    id "me.champeau.jmh" version "0.7.2"
}

- jmh 플러그인은 자동으로 자기가 사용할 버전의 org.openjdk.jmh 의 jmh-corejmh-generator-annprocess 를 설치하기 때문에 아래의 의존성을 추가하지 않아도 된다.

    - 임의로 지정한다면 지정한 jmh 버전이 대신 설치된다.

dependencies {
    jmh 'org.openjdk.jmh:jmh-core:1.36'
	jmh 'org.openjdk.jmh:jmh-generator-annprocess:1.36'
}

 

 

- jmh {}  안에는 벤치마크 할 때 Configuration Options를 설정할 수 있다.   (자세한건 아래 사이트 참고)

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 - melix/jmh-gradle-plugin

github.com

jmh{
    fork = 1
    warmupIterations = 1
    iterations = 1
}

이런 식으로 build.gradle에 추가하면 벤치마크 코드 전역으로 설정할 수 있고,

나는 벤치마크 코드에 어노테이션으로 직접 지정하였다.

 

tasks.withType(JavaCompile).configureEach {
    options.encoding = 'UTF-8'
}

❗ unmappable character 에러 해결

- UTF-8 인코딩 강제 지정


 

✅ 테스트 할 Java 코드

 

📂 src/main/java/패키지명/테스트할코드

프로그래머스 문제 풀이한 것을 넣어보았다.

 

2025.04.08 - [IT/Algorithm | Coding Test] - [프로그래머스 42586] [Java] 기능개발

 

[프로그래머스 42586] [Java] 기능개발

✏️ Solution 1import java.util.Arrays;import java.util.LinkedHashMap;import java.util.LinkedList;import java.util.Map;import java.util.Queue;public class 기능개발 { public int[] solution(int[] progresses, int[] speeds) { Queue q = new LinkedList();

iamsh.tistory.com

 

코드1    FuncDevelopment.java

package coding_test;

import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;

public class FuncDevelopment {

    public int[] solution(int[] progresses, int[] speeds) {
        Queue<Integer> q = new LinkedList<>();
        Map<Integer,Integer> map = new LinkedHashMap<>();

        for (int i=0; i<progresses.length; i++) {
            q.add(progresses[i]);
        }

        int n = 1;
        for (int i=0; i<progresses.length; i++) {
            while (true) {
                if (progresses[i] + speeds[i]*n >= 100) {
                    q.poll();
                    map.put(n, map.getOrDefault(n, 0)+1);
                    break;
                } else {
                    n++;
                }
            }
        }

        return map.values().stream().mapToInt(Integer::intValue).toArray();
    }
}

 

코드2    FuncDevelopment_2.java

package coding_test;

import java.util.ArrayList;
import java.util.List;

public class FuncDevelopment_2 {

    public int[] solution(int[] progresses, int[] speeds) {
        List<Integer> result = new ArrayList<>();
        int n = progresses.length;
        int[] days = new int[n];

        // 각 작업마다 걸리는 날짜 계산
        for (int i=0; i<n; i++) {
            days[i] = (int) Math.ceil( (100.0-progresses[i]) / speeds[i]);
        }

        // 앞에 있는 작업보다 늦게 끝나면 따로 배포, 아니면 같은 날 배포
        int cnt = 1;
        int prev = days[0];
        for (int i=1; i<n; i++) {
            if (days[i] <= prev) {
                cnt++;
            } else {
                result.add(cnt);
                cnt = 1;
                prev = days[i];
            }
        }
        result.add(cnt);
        
        return result.stream().mapToInt(Integer::intValue).toArray();
    }
}

 

✅ 벤치마크 클래스 생성

 

 📂 src/jmh/java/패키지명/벤치마크클래스

 

BenchmarkTest.java 

package coding_test;

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

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Warmup(iterations = 3)             // 워밍업 3회만
@Measurement(iterations = 5)        // 측정 5회만
@Fork(1)                            // JVM 실행 1번
@State(Scope.Thread)
public class BenchmarkTest {

    FuncDevelopment s1;
    FuncDevelopment_2 s2;

    int[] progresses;
    int[] speeds;

    @Setup(Level.Invocation)
    public void setup() {
        s1 = new FuncDevelopment();
        s2 = new FuncDevelopment_2();
        progresses = new int[]{20, 99, 93, 30, 55, 10, 90, 99, 100, 80, 70};
        speeds = new int[]{5, 10, 1, 1, 30, 5, 1, 1, 1, 1, 1};
    }

    @Benchmark
    public int[] solution1() {
        return s1.solution(progresses, speeds);
    }

    @Benchmark
    public int[] solution2() {
        return s2.solution(progresses, speeds);
    }
}

- JMH에서는 벤치마크용 클래스를 src/jmh/java 에 위치시켜야 한다

- build.gradle 에서 플러그인 추가한 것  ➡️  Gradle 이 src/jmh/java 폴더를 인식하고 빌드에 포함해준다.


@BenchmarkMode(Mode.AverageTime)

- 벤치마크 측정 방식 :  평균 실행 시간  (총 소요 시간 / 실행 횟수)


@OutputTimeUnit(TimeUnit.MICROSECONDS)

- 결과 출력 단위를 마이크로초(μs) 로 설정

 

@Warmup(iterations = 3)

- 처음 몇 번은 JVM이 코드 최적화를 덜 해서 느리게 측정된다
- 진짜 성능을 보기 위해 예열시킨다
- 일반적으로 3~5회


@Measurement(iterations = 5)

- 실제 성능을 측정하는 반복 횟수
- 너무 적으면 결과가 흔들린다 → 5~10회 


@Fork(1)

- JVM을 새로 켜서 반복을 측정한다
- 메모리/GC 상태 영향을 줄이기 위해 독립된 환경을 보장한다
- 일반적으로 1~2회


@State(Scope.Thread)

- 각 스레드마다 독립적인 상태(@Setup에서 설정한 값들)를 갖게 하겠다  → 병렬 측정에서도 간섭이 없게 만든다

 

@Setup(Level.Invocation)
- 각 벤치마크 메서드 호출 직전마다 실행될 초기화 함수 

    →  solution()가 실행될 때마다 setup()이 호출돼서 fresh한 상태를 보장한다 

 

@Benchmark
- 벤치마크 대상


|  디렉토리 구조 

app/
├── build
├── build.gradle
├── settings.gradle
└── src/
    ├── main/
    │   └── java/
    │   	└── coding_test/
    │       	├── FuncDevelopment.java
    │       	└── FuncDevelopment_2.java
    └── jmh/
        └── java/
        	└── coding_test/
            	└── BenchmarkTest.java

 

✅ 실행

 

|  빌드

gradlew clean build

- 프로젝트 정리하고 새로 빌드한다
- clean: 이전에 빌드된 파일들(build/ 디렉토리 등)을 삭제
   → 과거 결과가 현재 빌드에 영향을 주는 걸 방지
- build: 소스 코드 컴파일, 테스트, JAR 파일 등 빌드

 

|  벤치마크 실행

gradlew jmh

- JMH 실행해서 성능 벤치마크를 측정한다
- jmh는 미리 정의한 벤치마크 테스트 메서드들을 실행해서, 각 메서드의 성능(속도, GC, 처리량 등) 을 측정한다
- @Benchmark 어노테이션이 붙은 메서드들을 찾아 실행하고 결과를 출력한다

|  실행결과

c:\Users\hyeon\OneDrive\바탕 화면\HYEON\Coding_Test>gradlew jmh

BUILD SUCCESSFUL in 1s
7 actionable tasks: 7 up-to-date
c:\Users\hyeon\OneDrive\바탕 화면\HYEON\Coding_Test>gradlew jmh --rerun-tasks                                               

> Task :app:jmhRunBytecodeGenerator
Processing 1 classes from C:\Users\hyeon\OneDrive\바탕 화면\HYEON\Coding_Test\app\build\classes\java\jmh with "reflection" generator
Writing out Java source to C:\Users\hyeon\OneDrive\바탕 화면\HYEON\Coding_Test\app\build\jmh-generated-sources and resources to C:\Users\hyeon\OneDrive\바탕 화면\HYEON\Coding_Test\app\build\jmh-generated-resources
Processing 1 classes from C:\Users\hyeon\OneDrive\바탕 화면\HYEON\Coding_Test\app\build\classes\java\test with "reflection" generator
Writing out Java source to C:\Users\hyeon\OneDrive\바탕 화면\HYEON\Coding_Test\app\build\jmh-generated-sources and resources to C:\Users\hyeon\OneDrive\바탕 화면\HYEON\Coding_Test\app\build\jmh-generated-resources

> Task :app:jmh
# JMH version: 1.36
# VM version: JDK 21.0.6, OpenJDK 64-Bit Server VM, 21.0.6+7-LTS
# VM invoker: C:\Users\hyeon\.gradle\jdks\eclipse_adoptium-21-amd64-windows\jdk-21.0.6+7\bin\java.exe
# VM options: -Dfile.encoding=x-windows-949 -Djava.io.tmpdir=C:\Users\hyeon\OneDrive\바탕 화면\HYEON\Coding_Test\app\build\tmp\jmh -Duser.country=KR -Duser.language=ko -Duser.variant
# Blackhole mode: compiler (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
# Warmup: 3 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: coding_test.BenchmarkTest.solution1

# Run progress: 0.00% complete, ETA 00:02:40
# Fork: 1 of 1
# Warmup Iteration   1: 0.253 us/op
# Warmup Iteration   2: 0.246 us/op
# Warmup Iteration   3: 0.248 us/op
Iteration   1: 0.246 us/opING [34s]
Iteration   2: 0.248 us/opING [44s]
Iteration   3: 0.250 us/opING [54s]
Iteration   4: 0.253 us/opING [1m 4s]
Iteration   5: 0.251 us/opING [1m 14s]


Result "coding_test.BenchmarkTest.solution1":
  0.250 ±(99.9%) 0.010 us/op [Average]
  (min, avg, max) = (0.246, 0.250, 0.253), stdev = 0.003
  CI (99.9%): [0.240, 0.260] (assumes normal distribution)


# JMH version: 1.36
# VM version: JDK 21.0.6, OpenJDK 64-Bit Server VM, 21.0.6+7-LTS
# VM invoker: C:\Users\hyeon\.gradle\jdks\eclipse_adoptium-21-amd64-windows\jdk-21.0.6+7\bin\java.exe
# VM options: -Dfile.encoding=x-windows-949 -Djava.io.tmpdir=C:\Users\hyeon\OneDrive\바탕 화면\HYEON\Coding_Test\app\build\tmp\jmh -Duser.country=KR -Duser.language=ko -Duser.variant
# Blackhole mode: compiler (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
# Warmup: 3 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: coding_test.BenchmarkTest.solution2

# Run progress: 50.00% complete, ETA 00:01:20
# Fork: 1 of 1
# Warmup Iteration   1: 0.102 us/op5s]
# Warmup Iteration   2: 0.096 us/op5s]
# Warmup Iteration   3: 0.096 us/op5s]
Iteration   1: 0.098 us/opING [1m 55s]
Iteration   2: 0.098 us/opING [2m 5s]
Iteration   3: 0.095 us/opING [2m 15s]
Iteration   4: 0.096 us/opING [2m 25s]
Iteration   5: 0.096 us/opING [2m 35s]


Result "coding_test.BenchmarkTest.solution2":
  0.097 ±(99.9%) 0.005 us/op [Average]
  (min, avg, max) = (0.095, 0.097, 0.098), stdev = 0.001
  CI (99.9%): [0.092, 0.101] (assumes normal distribution)


# Run complete. Total time: 00:02:40

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
BenchmarkTest.solution1  avgt    5  0.250 ± 0.010  us/op
BenchmarkTest.solution2  avgt    5  0.097 ± 0.005  us/op

Benchmark result is saved to C:\Users\hyeon\OneDrive\바탕 화면\HYEON\Coding_Test\app\build\results\jmh\results.txt

BUILD SUCCESSFUL in 2m 46s
7 actionable tasks: 7 executed

 

( 결과가 txt 파일에 저장된다 )

Benchmark                Mode  Cnt  Score   Error  Units
BenchmarkTest.solution1  avgt    5  0.250 ± 0.010  us/op
BenchmarkTest.solution2  avgt    5  0.097 ± 0.005  us/op

 

해석

- solution2가 solution1보다 거의 2배 더 빠르다
- 편차(표준편차)가 작고 안정적이다 → 결과 신뢰도가 높다
- 평균 ± 오차범위 = 안정적인 측정임을 의미한다

Mode: avgt 평균 실행 시간 측정 기준
Cnt 반복 횟수 
Score 평균 실행 시간 (us/op → 마이크로초/1회 실행당)
Error 측정 오차 (±)
us/op 마이크로초(μs) per operation (한 번 실행하는 데 걸린 시간)

 

업데이트할 작업이 없으면 결과가 안 나오기도 한다  =>  강제 재실행하기 

gradlew jmh --rerun-tasks

 

 

 

 


refer to

https://velog.io/@anak_2/Java-JMH-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%84%B1%EB%8A%A5%ED%85%8C%EC%8A%A4%ED%8A%B8

 

'IT > JAVA' 카테고리의 다른 글

Java String 클래스  (0) 2025.04.15
ArrayList vs LinkedList  (0) 2025.04.08
코드 실행시간 비교  (0) 2025.03.26
HashSet  (0) 2025.03.20
Pattern, Matcher Class  (0) 2025.02.25