본문 바로가기

스터디/자바

JMH 사용해보기

이펙티브 자바를 읽던도중 JMH 라는 용어가 나왔다. 책에서는 간단하게 '자바코드의 상세한 성능을 알기 쉽게 보여주는 마이크로 벤치마킹 프레임워크다.' 라고 설명한다.

새로운 녀석이 등장하기도 하였고 JMH에 대한 이펙티브 자바 질문 이슈가 달렸기에 JMH에 대해 간단하게 알아보고 적용하기로 하였다.

JMH란?

JMH는 OpenJDK에서 개발한 성능 측정 툴이다. 특정 메소드의 성능을 측정하는 식으로 사용할 수 있고 실제 테스트하기전 워밍업 과정과 실제 측정 과정을 수행하는데 각 과정의 실행 수를 제어할 수 있고, 측정 후 결과로 나오는 시간의 단위를 지정하는 기능도 제공한다.

적용하기

JMH 적용을 gradle을 이용하여 적용하기로 하였다. 다음부터 설명할 JMH gradle 적용은 다음을 참고하여 적용하였다.

https://github.com/melix/jmh-gradle-plugin

build.gradle 설정

jmh-gradle-plugin 을 사용하고 버전은 현재 최신 버전인 0.5.0을 사용하기로 하였다. jmh-gradle-plugin 버전 0.5.0을 사용하기 위해선 gradle 버전 5.5 이상을 요구하기 때문에 gradle 버전을 5.5.1 로 바꾸고 사용하였다.

gradle-wrapper.properties

jmh-gradle-plugin을 plugin에 추가해주고 dependencies 에 필요한 의존성을 추가하였다.

plugins {
    id 'java'
    id 'me.champeau.gradle.jmh' version '0.5.0'
}

group 'org.example'
version '1.0-SNAPSHOT'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    jmh 'org.openjdk.jmh:jmh-core:0.9'
    jmh 'org.openjdk.jmh:jmh-generator-annprocess:1.12'

    testCompile group: 'junit', name: 'junit', version: '4.12'
}

 

프로젝트 구조

jmh-gradle-plugin 레포의 readme를 보면 프로젝트 구조를 '플러그인을 사용하면 특정 구성 덕분에 기존 프로젝트에 쉽게 통합 할 수 있습니다. 특히 벤치 마크 소스 파일은 src / jmh 디렉토리에 있습니다' 라는 설명과 함께 src/jmh 구조에 벤치마크할 클래스들을 두게 하도록 한다.

프로젝트 구조

BenchMark 메소드 생성하기

이제 성능을 측정할 BenchMark 메소드를 생성해보자, src/jmh 하위에 적당히 패키지를 만들고 성능을 측정할 메소드들을 가질 클래스를 만든다. 
다음으로 클래스에 메소드를 생성하고 메소드 내부에 성능을 측정하고 싶은 로직을 담는다. 
OpenJDK의 JMH 공식 문서에 여러개의 샘플 코드가 있지만, Enum 클래스를 키로할때 EnumMap과 HashMap의 성능을 테스트 하기로해서 샘플코드를 참고하여 BenchMark 메소드를 만들었다.

@State(Scope.Thread)
public class EnumMapBenchMark {
    private Map<State, String> enumMap;
    private Map<State, String> hashMap;

    @Setup
    public void setUp() {
        enumMap = new EnumMap<>(State.class);
        hashMap = new HashMap<>();
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
	@OutputTimeUnit(TimeUnit.NANOSECONDS)
    public void enumMap(Blackhole blackhole) {
        blackhole.consume(enumMap.put(State.SOLID, State.SOLID.name()));
    }

    @Benchmark
    @BenchmarkMode(Mode.AverageTime)
	@OutputTimeUnit(TimeUnit.NANOSECONDS)
    public void hashMap(Blackhole blackhole) {
        blackhole.consume(hashMap.put(State.SOLID, State.SOLID.name()));
    }

    enum State {
        SOLID, LIQUID, GAS;
    }
}

@State 는 측정 대상의 상태를 지정할때 사용한다. Thread는 쓰레드별로 인스턴스를 생성하고, BenchMark 는 동일한 테스트 내의 모든 쓰레드에서 동일한 인스턴스를 공유한다. 

@Setup 어노테이션은 벤치마크 대상이 되는 메소드를 실행하기 전에 수행할 메소드에 사용한다. EnumMap과 HashMap의 인스턴스 생성이 각각의 성능 측정에 영향을 미칠것 같다고 생각해서 해당 어노테이션을 이용해서 수행전에 생성하도록 하였다.

@BenchMark 는 성능 측정에 대상이 될 메소드에 사용한다. @BenchmarkMode 로 벤치마크 모드를 설정할 수 있다. 나는 AverageTime을 주어 각각의 기능을 수행하는데 걸리는 평균 시간을 측정하도록 하였다.

@OutputTimeUnit 은 성능 측정 후 결과를 보여줄때 사용할 시간의 단위를 지정할 수 있다.

다음으로 볼것은 Blackhole 객체인데, JVM은 메소드 내에서 실행된 코드가 Side Effect 를 일으키지 않고 그 결과를 사용하지 않는다면 해당 메소드를 삭제 대상으로 삼는다고 한다. 따라서 blackhole 객체를 주입받아서 성능 측정에 영향을 미치는 것을 최소한으로 하면서 삭제 대상이 되는 것을 방지한다.

BenchMark 수행

이제 BenchMark를 수행해보자. 터미널에 gradle jmh 로 jmh task를 수행하면 BenchMark 수행 설정값들이 기본값으로 설정되어 수행된다. 따라서 각 벤치마크 메소드를 워밍업 수행 5번, 실제 측정 5번을 fork 1회로 하고 , 5회의 fork를 수행한다. 

build.gradle에 jmh 옵션을 주어 워밍업 수행 횟수와, 측정 횟수, fork 횟수를 바꿔보자.

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

build.gradle 의 설정을 바꾸고 터미널에 gradle jmh 로 jmh task 를 수행하였다.

jmh task
측정 결과

측정 결과를 보면, enumMap 이 평균 수행 시간이 더 빠른것으로 측정 되었다. 

또 측정 결과를 텍스트 파일로 만들어서 build/reports/jmh 디렉토리에 저장한다.

다른 이야기...

jmh의 설정을 바꾸는 방법이 공식 문서에서는 Runner를 사용하는 방법이 있다. 

public static void main(String[] args) throws IOException, RunnerException {
        Options options = new OptionsBuilder()
                .include(EnumMapBenchMark.class.getSimpleName())
                .warmupIterations(2)
                .measurementIterations(2)
                .forks(1)
                .build();

        new Runner(options).run();
}

이렇게 하면 IDE의 메인 메소드 실행으로 간단하게 수행할 수 있다고 했었는데, 실행을 해보니 빌드 오류가 났었다.

'빌드 오류...

Runner가 있는 것을 보면 분명히 메인 메소드 실행으로 가능할것 같은데 왜 안될까 생각하다가 jmh-gralde-plugin 의 readme 엔 Runner에 대한 설명이 없는것과 다른 gradle 적용 포스팅에서도 나와 같이 메인 메소드 실행으로 오류가 발생하는 것을 보고 한번 maven으로 하면 어떨까? 하고 같은 조건으로 맞추고 실행해보았다.

되는데요.

된다...해당 plugin의 이슈에서 runner에 관한 것을 찾아봐도 보이지 않는다. runner로 실행하는 것은 다음 기회에 찾아보도록 해야겠다.

마무리

이펙티브 자바를 읽고 정리하다보면 항상 새롭고 몰랐던 것들이 튀어나온다. 이번 글의 주제인 jmh도 간략하게만 언급되서 그냥 지나칠 수 도 있었는데 jmh에 대한 질문이 나와서 찾아보고 사용해보게 되었다.

jmh를 간단하게 사용하면서 OpenJDK의 샘플 코드가 아니라 이번에 성능 측정을 한 것 처럼 책에서 '어떤것이 어떤것보다 성능이 더 좋다, 라는 것들을 직접 확인할 수 있어서 더욱 깊게 남는것 같다.