소프트웨어 엔지니어로서 자신이 만든 로직 또는 알고리즘이나 3rd party 라이브러리의 성능을 측정하고 비교 분석하고 싶을 때가 있습니다. 무심코 작성한 코드가 성능에 영향을 미칠 수 있어서 특정 동작을 하는 로직을 다른 방식으로 여러개 만들어 벤치마킹 할 수도 있고 그냥 지적 호기심에 특정 라이브러리의 성능을 다른 경쟁 라이브러리의 성능과 비교 분석하고자 할 수도 있습니다. JAVA로 구현된 코드를 측정할 때 가장 공신력있는 벤치마킹 결과를 얻고자 한다면 JMH(Java Microbenchmark Harness)를 이용하는걸 추천합니다.
- 왜 JMH로 벤치마킹 해야하는가?
JVM을 만든 사람들이 JMH를 만들었습니다. 벤치마킹 환경 구성이나 실제 성능 측정에 영향을 미칠만한 부가적인 요소를 최소화 했겠죠? 메소드가 JVM에서 실행될 때 JIT 컴파일러는 최적화를 합니다. 어느 부분에 최적화 할곳이 있는지, inline method로 만들부분이 있는지, 중복 코드가 있는지, dead code가 있는지, 코드 reordering 할 부분이 있는지 등을 분석해 최적화를 하기 때문에 제대로된 벤치마킹을 하기가 어렵습니다. JMH를 이용하면 컴파일러의 최적화 작업을 어느정도 조정할 수 있고 코드 웜업을 한 상태에서 메소드들을 측정하기 때문에 측정 오류를 최소화 할 수 있습니다. 그리고 공식적으로 JAVA 9에 벤치마킹 툴로 배포된다고 합니다.
- JMH 사용법
아래 참고에 있는 링크에서 학습하시면 됩니다. 그 중 Jakob Jenkov가 작성한 tutorial이 가장 읽기 편합니다. http://tutorials.jenkov.com/java-performance/jmh.html 여길 참고하세요.
- 간단한 벤치마킹 코드를 짜보자.
(Github[https://github.com/jayjaykim/jmh-sample]에 에 소스 올려 놓았으니 앞으로 설명할 샘플 코드를 다운 받아 참고 하시면 됩니다.)
JMH를 이용하여 벤치마킹 코드를 구현하려면 maven build system을 사용해야 합니다. https://maven.apache.org/ 에서 maven을 다운 받고 실행 파일 경로 설정을 한 후 아래와 같이 콘솔에 입력합니다. 하기 명령어의 각 옵션 사이가 라인피드로 되어 있는데요 가독성을 위해 편의상 그렇게 한것이고 실제로는 그냥 스페이스 처리 하시면 됩니다.
$ mvn archetype:generate -DinteractiveMode=false -DarchetypeGroupId=org.openjdk.jmh -DarchetypeArtifactId=jmh-java-benchmark-archetype -DgroupId=com.jayjaylab.sample -DartifactId=jmh-sample -Dversion=1
이렇게 하면 JAVA maven project가 하나 만들어 지는데, 바로 아래와 같이 명령어 입력해 주시면 벤치마킹이 수행이 됩니다. 자세한 사항은 참고[3](http://tutorials.jenkov.com/java-performance/jmh.html)를 따라하시면 쉽게 됩니다.^^
이렇게 하면 jmh를 이용해 벤치마킹을 실행할 수 있습니다. 벤치마킹 모드를 Mode.Throughput으로 하면 한 메소드 당 기본 10번 세팅(한 세트에 20 warmup iteration, 20 실제 iteration)을 돌립니다.
재미로 그럼 벤치마킹 코드를 작성해 보겠습니다. 간단히 1부터 10,000까지 합을 구하는 로직을 for loop을 이용해 구하는 법과 JAVA 8의 IntStream을 이용해 구하는 법을 벤치마킹합니다. 아래 코드의 testSummingByForLoop(), testSummingByStream() 메소드는 각각 for loop과 IntStream으로 합계를 구하는 메소드입니다.
public class StreamBenchmark { @org.openjdk.jmh.annotations.State(Scope.Thread) public static class State { int startNumber; int endNumber; @Setup(Level.Trial) public void setUp() { startNumber = 0; endNumber = 10000; } @TearDown(Level.Trial) public void tearDown() { } } @Benchmark @BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.SECONDS) public void testSummingByForLoop(State state, Blackhole blackhole) { long sum = 0l; for(int i = state.startNumber; i < state.endNumber; i++) { sum += i; } blackhole.consume(sum); } @Benchmark @BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.SECONDS) public void testSummingByStream(State state, Blackhole blackhole) { blackhole.consume( IntStream.range(state.startNumber, state.endNumber).sum()); } }
위 코드를 빌드하고 벤치마킹을 실행하면 하기와 같은 결과를 얻습니다. for-loop 확실히 약 7배 빠르군요. (그런데 정확히 7배라고 말하기 어렵습니다. 원래 벤치마킹할 때는 벤치마킹만 돌리고 다른 프로그램을 실행하면 안되는데 저는 이것저것 하면서 돌린것이라서;;). 참고로 본 벤치마킹을 실행한 환경은 MAC OS X yosemite version 10.10.5(14F27), 2.7GHz Intel core i5, 8GB 1867MHz DD3 입니다. Throughput으로도 충분히 해석할 수 있지만 @BenchmarkMode를 Mode.AverageTime으로 변경하여 각 메소드가 얼마나 걸리는지 알수 있습니다. 아래 결과를 좀더 보시면 min, max, mean을 구하고 표준편차와 분포를 normal distribution을 가정했을 때 confidence interval도 계산되어 나옵니다(confidence level : 99.9%, margin of error : (41011.042, 42081.252]). 이 부분에 대한 해석은 통계에 대한 지식이 있어야겠지요? 통계도 아셔야합니다. 저는 대학교, 대학원때 공부했는데 아직도 가물가물해서 다시 들여다 보고 있네요^^;;
Result "testSummingByForLoop": 280454.648 ±(99.9%) 2181.860 ops/s [Average] (min, avg, max) = (227831.822, 280454.648, 289601.359), stdev = 9238.133 CI (99.9%): [278272.788, 282636.509] (assumes normal distribution) Result "testSummingByStream": 41546.147 ±(99.9%) 535.105 ops/s [Average] (min, avg, max) = (25794.615, 41546.147, 43398.632), stdev = 2265.668 CI (99.9%): [41011.042, 42081.252] (assumes normal distribution) # Run complete. Total time: 00:13:28 Benchmark Mode Cnt Score Error Units StreamBenchmark.testSummingByForLoop thrpt 200 280454.648 ± 2181.860 ops/s StreamBenchmark.testSummingByStream thrpt 200 41546.147 ± 535.105 ops/s
어떤가요? 벤치마킹이 참 편해졌습니다. 자 그럼 JMH를 써보시고, 저는 나중 나중에 좀더 재밌는 벤치마킹을 들고 #2를 작성해 올리겠습니다.
참고
[1] http://psy-lob-saw.blogspot.kr/2013/04/writing-java-micro-benchmarks-with-jmh.html
'java' 카테고리의 다른 글
JAVA 8 Fuctional Inteface, Method reference and Lambda expression #1 (0) | 2016.04.30 |
---|---|
추상화(Abstraction) 이해의 중요성 #3 (0) | 2016.03.27 |
추상화(Abstraction) 이해의 중요성 #2 (0) | 2016.03.06 |
추상화(Abstraction) 이해의 중요성 #1 (0) | 2016.03.02 |
단순한 Dependency Injector를 구현해 써보자 #1 (0) | 2016.02.29 |