어디에 담아야 하는지
JMH 설치 및 설정 방법
JMH는 JDK를 오픈 소스로 제공하는 OpenJDK에서 만든 성능 측정용 라이브러리다.
소스 받기 먼저 Mercurial이라는 분산 저장소에 접근하는 hg라는 툴을 이용하여 소스 코드를 받는다. url : https://www.mercurial-scm.org/downloads
hg 설치를 마쳤으면 원하는 디렉터리에서 다음 명령을 실행한다.
$ hg clone http://hg.openjdk.java.net/code-tools/jmh/ jmh
정상적으로 코드 다운로드가 완료되었으면 다음의 명령을 사용하여 메이븐 빌드를 실행한다.
$ cd jmh
$ mvn clean install -DskipTests=true
정상적으로 프로젝트 빌드가 완료되었다면 메이븐 로컬 저장소에 JMH 라이브러리가 등록되어 있을 것이다.
그리고 동일 디렉토리에서 벤치마크 프로젝트를 빌드한다.
$ mvn archetype:generate -DinteractiveMode=false -DarchetypeGroupId=org.openjdk.jmh -DarchetypeArtifactId=jmh-java-benchmark-archetype -DgroupId=org.sample -DartifactId=test -Dversion=1.0
마지막으로 test 디렉토리에 있는 것을 빌드하면 기본적인 설정은 끝난다.
$ cd test
$ mvn clean install
간단한 예제
@BenchmarkMode({Mode.AverageTime})
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class MyBenchmark {
@Benchmark
public DummyData makeObjectWithSize() {
HashMap<String, String> map = new HashMap<>(1000000);
ArrayList<String> list = new ArrayList<>(1000000);
return new DummyData(map, list);
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(MyBenchmark.class.getSimpleName())
.warmupIterations(5)
.measurementIterations(5)
.forks(1)
.build();
new Runner(opt).run();
}
}
@Benchmark
라는 애노테이션을 메서드에 선언하면 JMH에서 측정 대상 코드라고 인식한다.
하나의 클래스에 여러 개의 메서드가 존재할 수 있다.
애노테이션을 선언한 메서드가 끝나지 않으면 측정도 끝나지 않는다.
예외가 발생할 경우 해당 메서드의 측정을 종료하고, 다음 측정 메서드로 이동한다.
위의 수행 결과는 다음과 같다.
# JMH version: 1.19
# VM version: JDK 1.8.0_60, VM 25.60-b23
# VM invoker: /Library/Java/JavaVirtualMachines/jdk1.8.0_60.jdk/Contents/Home/jre/bin/java
# VM options: -Didea.launcher.port=7537 -Didea.launcher.bin.path=/Volumes/IntelliJ IDEA/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8
# Warmup: 5 iterations, 1 s each
# Measurement: 5 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: org.sample.MyBenchmark.makeObjectWithSize
# Run progress: 0.00% complete, ETA 00:00:10
# Fork: 1 of 1
# Warmup Iteration 1: 1.608 ms/op
# Warmup Iteration 2: 0.612 ms/op
# Warmup Iteration 3: 0.564 ms/op
# Warmup Iteration 4: 0.572 ms/op
# Warmup Iteration 5: 0.553 ms/op
Iteration 1: 0.571 ms/op
Iteration 2: 0.555 ms/op
Iteration 3: 0.558 ms/op
Iteration 4: 0.557 ms/op
Iteration 5: 0.557 ms/op
Result "org.sample.MyBenchmark.makeObjectWithSize":
0.560 ±(99.9%) 0.025 ms/op [Average]
(min, avg, max) = (0.555, 0.560, 0.571), stdev = 0.007
CI (99.9%): [0.534, 0.585] (assumes normal distribution)
# Run complete. Total time: 00:00:10
Benchmark Mode Cnt Score Error Units
MyBenchmark.makeObjectWithSize avgt 5 0.560 ± 0.025 ms/op
Process finished with exit code 0
Set 클래스 중 무엇이 가장 빠를까?
먼저 HashSet, TreeSet, LinkedHashSet add를 비교해보면 다음과 같다.
@State(Scope.Thread)
@BenchmarkMode({Mode.AverageTime})
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public class SetAdd {
int LOOP_COUNT = 1000;
Set<String> set;
String data = "abcdefghijklmnopqrstuvwxyz";
@Benchmark
public void addHashSet() {
set = new HashSet<>();
for (int loop = 0; loop < LOOP_COUNT; loop++) {
set.add(data + loop);
}
}
@Benchmark
public void addTreeSet() {
set = new TreeSet<>();
for (int loop = 0; loop < LOOP_COUNT; loop++) {
set.add(data + loop);
}
}
@Benchmark
public void addLinkedHashSet() {
set = new LinkedHashSet<>();
for (int loop = 0; loop < LOOP_COUNT; loop++) {
set.add(data + loop);
}
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(SetAdd.class.getSimpleName())
.warmupIterations(3)
.measurementIterations(5)
.forks(1)
.build();
new Runner(opt).run();
}
}
# Run complete. Total time: 00:00:25
Benchmark Mode Cnt Score Error Units
SetAdd.addHashSet avgt 5 79.923 ± 7.503 us/op
SetAdd.addLinkedHashSet avgt 5 84.106 ± 10.006 us/op
SetAdd.addTreeSet avgt 5 297.550 ± 56.801 us/op
HashSet과 LinkedHashSet의 성능이 비슷하고, TreeSet은 성능 차이가 발생한다 TreeSet은 레드블랙 트리에 데이터를 담는다. 값에 따라서 순서가 정해진다. 데이터를 담으면서 동시에 정렬을 하기 때문에 HashSet보다 성능상 느리다.
이번에는 Set 클래스들이 데이터를 읽을 때 얼마나 많은 차이가 발생하는지 확인해보자.
@State(Scope.Thread)
@BenchmarkMode({Mode.AverageTime})
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public class SetIterate {
int LOOP_COUNT = 1000;
Set<String> hashSet;
Set<String> treeSet;
Set<String> linkedHashSet;
String data = "abcdefghijklmnopqrstuvwxyz";
String[] keys;
String result = null;
@Setup(Level.Trial)
public void setUp() {
hashSet = new HashSet<>();
treeSet = new TreeSet<>();
linkedHashSet = new LinkedHashSet<>();
for (int loop = 0; loop < LOOP_COUNT; loop++) {
String tempData = data+loop;
hashSet.add(tempData);
treeSet.add(tempData);
linkedHashSet.add(tempData);
}
}
@Benchmark
public void iterateHashSet() {
Iterator<String> iter = hashSet.iterator();
while (iter.hasNext()) {
result = iter.next();
}
}
@Benchmark
public void iterateTreeSet() {
Iterator<String> iter = treeSet.iterator();
while (iter.hasNext()) {
result = iter.next();
}
}
@Benchmark
public void iterateLinkedHashSet() {
Iterator<String> iter = linkedHashSet.iterator();
while (iter.hasNext()) {
result = iter.next();
}
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(SetIterate.class.getSimpleName())
.warmupIterations(3)
.measurementIterations(5)
.forks(1)
.build();
new Runner(opt).run();
}
# Run complete. Total time: 00:00:25
Benchmark Mode Cnt Score Error Units
SetIterate.iterateHashSet avgt 5 8.154 ± 2.364 us/op
SetIterate.iterateLinkedHashSet avgt 5 11.204 ± 0.577 us/op
SetIterate.iterateTreeSet avgt 5 12.678 ± 1.558 us/op
읽기에서는 크게 차이나지 않는다. Set을 Iterator 돌려서 사용하기도 하지만, 일반적으로 Set은 여러 데이터를 넣어 두고 해당 데이터가 존재하는지를 확인하는 용도로 많이 사용된다. 따라서 데이터를 Iterator로 가져오는 것이 아니라, 랜덤하게 가져와야만 한다.
@State(Scope.Thread)
@BenchmarkMode({Mode.AverageTime})
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public class SetContains {
int LOOP_COUNT = 1000;
Set<String> hashSet;
Set<String> treeSet;
Set<String> linkedHashSet;
String data = "abcdefghijklmnopqrstuvwxyz";
String[] keys;
String result = null;
@Setup(Level.Trial)
public void setUp() {
hashSet = new HashSet<>();
treeSet = new TreeSet<>();
linkedHashSet = new LinkedHashSet<>();
for (int loop = 0; loop < LOOP_COUNT; loop++) {
String tempData = data+loop;
hashSet.add(tempData);
treeSet.add(tempData);
linkedHashSet.add(tempData);
}
if (keys == null || keys.length != LOOP_COUNT) {
keys = RandomKeyUtil.generateRandomSetKeysSwap(hashSet);
}
}
@Benchmark
public void containsHashSet() {
for (String key : keys) {
hashSet.contains(key);
}
}
@Benchmark
public void containsTreeSet() {
for (String key : keys) {
treeSet.contains(key);
}
}
@Benchmark
public void containsLinkedHashSet() {
for (String key : keys) {
linkedHashSet.contains(key);
}
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(SetContains.class.getSimpleName())
.warmupIterations(3)
.measurementIterations(5)
.forks(1)
.build();
new Runner(opt).run();
}
}
public class RandomKeyUtil {
public static String[] generateRandomSetKeysSwap(Set<String> set) {
int size = set.size();
String[] result = new String[size];
Random random = new Random();
int maxNumber = size;
Iterator<String> iterator = set.iterator();
int resultPos = 0;
while (iterator.hasNext()) {
result[resultPos++] = iterator.next();
}
for (int loop = 0; loop < size; loop++) {
int randomNumber1 = random.nextInt(maxNumber);
int randomNumber2 = random.nextInt(maxNumber);
String temp = result[randomNumber2];
result[randomNumber2] = result[randomNumber1];
result[randomNumber1] = temp;
}
return result;
}
public static int[] generateRandomNumberKeysSwap(int loop_count) {
int[] result = new int[loop_count];
Random random = new Random();
for (int i=0; i< loop_count; i++) {
int randomNumber = random.nextInt(loop_count);
result[i] = randomNumber;
}
return result;
}
}
# Run complete. Total time: 00:00:26
Benchmark Mode Cnt Score Error Units
SetContains.containsHashSet avgt 5 9.446 ± 2.284 us/op
SetContains.containsLinkedHashSet avgt 5 9.875 ± 4.718 us/op
SetContains.containsTreeSet avgt 5 239.340 ± 88.700 us/op
HashSet과 LinkedHashSet의 속도는 빠르지만, TreeSet의 속도는 느리다는 것을 알 수 있다. 그러면 왜 결과가 항상 느리게 나오는 TreeSet 클래스를 만들었을까?
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable
TreeSet은 데이터를 저장하면서 정렬한다. 구현한 인터페이스 중에 NavigableSet이 있다. 이 인터페이스는 특정 값보다 큰 값이나 작은 값, 가장 큰 값, 가장 작은 값 등을 추출하는 메서드를 선언해 놓았으며 JDK 1.6부터 추가된 것이다. 즉, 데이터를 순서에 따라 탐색하는 작업이 필요할때는 TreeSet을 사용하는 것이 좋다는 의미다. 하지만 그럴 필요가 없을 때는 HashSet이나 LinkedHashSet을 사용하는 것을 권장한다.
List 관련 클래스 중 무엇이 빠를까?
데이터를 넣는 속도부터 비교해보자.
@State(Scope.Thread)
@BenchmarkMode({Mode.AverageTime})
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public class ListAdd {
int LOOP_COUNT = 1000;
List<Integer> arrayList;
List<Integer> vector;
List<Integer> linkedList;
@Benchmark
public void addArrayList() {
arrayList = new ArrayList<>();
for (int loop = 0; loop < LOOP_COUNT; loop++) {
arrayList.add(loop);
}
}
@Benchmark
public void addVector() {
vector = new Vector<>();
for (int loop = 0; loop < LOOP_COUNT; loop++) {
vector.add(loop);
}
}
@Benchmark
public void addLinkedList() {
linkedList = new LinkedList<>();
for (int loop = 0; loop < LOOP_COUNT; loop++) {
linkedList.add(loop);
}
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(ListAdd.class.getSimpleName())
.warmupIterations(3)
.measurementIterations(5)
.forks(1)
.build();
new Runner(opt).run();
}
}
# Run complete. Total time: 00:00:26
Benchmark Mode Cnt Score Error Units
ListAdd.addArrayList avgt 5 12.038 ± 3.928 us/op
ListAdd.addLinkedList avgt 5 11.656 ± 8.859 us/op
ListAdd.addVector avgt 5 12.376 ± 21.172 us/op
어떤 클래스든 큰 차이가 없다는 것을 알 수 있다.
이번에는 데이터를 꺼내는 속도를 확인해보자.
@State(Scope.Thread)
@BenchmarkMode({Mode.AverageTime})
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public class ListGet {
int LOOP_COUNT = 1000;
List<Integer> arrayList;
List<Integer> vector;
List<Integer> linkedList;
int result = 0;
@Setup
public void setUp() {
arrayList = new ArrayList<>();
vector = new Vector<>();
linkedList = new LinkedList<>();
for (int loop = 0; loop < LOOP_COUNT; loop++) {
arrayList.add(loop);
vector.add(loop);
linkedList.add(loop);
}
}
@Benchmark
public void getArrayList() {
for (int loop = 0; loop < LOOP_COUNT; loop++) {
result = arrayList.get(loop);
}
}
@Benchmark
public void getVector() {
for (int loop = 0; loop < LOOP_COUNT; loop++) {
result = vector.get(loop);
}
}
@Benchmark
public void getLinkedList() {
for (int loop = 0; loop < LOOP_COUNT; loop++) {
result = linkedList.get(loop);
}
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(ListGet.class.getSimpleName())
.warmupIterations(3)
.measurementIterations(5)
.forks(1)
.build();
new Runner(opt).run();
}
}
# Run complete. Total time: 00:00:26
Benchmark Mode Cnt Score Error Units
ListGet.getArrayList avgt 5 1.282 ± 0.243 us/op
ListGet.getLinkedList avgt 5 435.129 ± 105.285 us/op
ListGet.getVector avgt 5 29.920 ± 6.519 us/op
ArrayList의 속도가 가장 빠르고, Vector와 LinkedList는 속도가 매우 느리다. LinkedList가 터무니없이 느리게 나온 이유는 LinkedList가 Queue 인터페이스를 상속받기 때문이다. 이를 수정하기 위해서는 순차적으로 결과를 받아오는 peek() 메서드를 사용해야 한다.
@Benchmark
public void getPeekLinkedList() {
for (int loop = 0; loop < LOOP_COUNT; loop++) {
result = linkedList.peek();
}
}
# Run complete. Total time: 00:00:34
Benchmark Mode Cnt Score Error Units
ListGet.getArrayList avgt 5 1.242 ± 0.180 us/op
ListGet.getLinkedList avgt 5 425.459 ± 54.941 us/op
ListGet.getPeekLinkedList avgt 5 0.038 ± 0.003 us/op
ListGet.getVector avgt 5 27.923 ± 0.632 us/op
LinkedList 클래스를 사용할 때는 get() 메서드가 아닌 peek()이나 poll() 메서드를 사용해야 한다. 그런데 왜 ArrayList와 Vector의 성능 차이가 이렇게 클까? ArrayList는 여러 스레드에서 접근할 경우 문제가 발생할 수 있지만, Vector는 여러 스레드에서 접근할 경우를 방지하기 위해서 get() 메서드에 synchronized가 선언되어 있다. 따라서 성능 저하가 발생할 수 밖에 없다.
마지막으로 데이터를 삭제하는 속도를 비교해보자.
@State(Scope.Thread)
@BenchmarkMode({Mode.AverageTime})
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public class ListRemove {
int LOOP_COUNT = 10;
List<Integer> arrayList;
List<Integer> vector;
LinkedList<Integer> linkedList;
int result = 0;
@Setup(Level.Trial)
public void setUp() {
arrayList = new ArrayList<>();
vector = new Vector<>();
linkedList = new LinkedList<>();
for (int loop = 0; loop < LOOP_COUNT; loop++) {
arrayList.add(loop);
vector.add(loop);
linkedList.add(loop);
}
}
@Benchmark
public void removeArrayListFromFirst() {
ArrayList<Integer> tempList = new ArrayList<>(arrayList);
for (int loop = 0; loop < LOOP_COUNT; loop++) {
tempList.remove(0);
}
}
@Benchmark
public void removeArrayListFromLast() {
ArrayList<Integer> tempList = new ArrayList<>(arrayList);
for (int loop = LOOP_COUNT -1 ; loop >= 0; loop--) {
tempList.remove(loop);
}
}
@Benchmark
public void removeVectorFromFirst() {
List<Integer> tempList = new Vector<>(vector);
for (int loop = 0; loop < LOOP_COUNT; loop++) {
tempList.remove(0);
}
}
@Benchmark
public void removeVectorFromLast() {
List<Integer> tempList = new Vector<>(vector);
for (int loop = LOOP_COUNT -1 ; loop >= 0; loop--) {
tempList.remove(loop);
}
}
@Benchmark
public void removeLinkedListFromFirst() {
LinkedList<Integer> tempList = new LinkedList<>(linkedList);
for (int loop = 0; loop < LOOP_COUNT; loop++) {
tempList.remove(0);
}
}
@Benchmark
public void removeLinkedListFromLast() {
LinkedList<Integer> tempList = new LinkedList<>(linkedList);
for (int loop = LOOP_COUNT -1 ; loop >= 0; loop--) {
tempList.remove(loop);
}
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(ListRemove.class.getSimpleName())
.warmupIterations(3)
.measurementIterations(5)
.forks(1)
.build();
new Runner(opt).run();
}
}
# Run complete. Total time: 00:00:51
Benchmark Mode Cnt Score Error Units
ListRemove.removeArrayListFromFirst avgt 5 0.089 ± 0.012 us/op
ListRemove.removeArrayListFromLast avgt 5 0.023 ± 0.001 us/op
ListRemove.removeLinkedListFromFirst avgt 5 0.161 ± 0.094 us/op
ListRemove.removeLinkedListFromLast avgt 5 0.137 ± 0.021 us/op
ListRemove.removeVectorFromFirst avgt 5 0.097 ± 0.003 us/op
ListRemove.removeVectorFromLast avgt 5 0.038 ± 0.007 us/op
결과를 보면 첫 번째 값을 삭제하는 메서드와 마지막 값을 삭제하는 메서드의 속도 차이는 크다. 그리고 LinkedList는 별 차이가 없다. 그 이유가뭘까? ArrayList와 Vector는 실제로 그 안에 배열을 사용하기 때문에 0번째 인덱스 값을 삭제하면 나머지를 다 옮겨야 하기 때문이다.
Map 관련 클래스 중에서 무엇이 빠를까?
@State(Scope.Thread)
@BenchmarkMode({Mode.AverageTime})
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public class MapGet {
int LOOP_COUNT = 1000;
Map<Integer, String> hashMap;
Map<Integer, String> hashTable;
Map<Integer, String> treeMap;
Map<Integer, String> linkedHashMap;
int[] keys;
@Setup(Level.Trial)
public void setUp() {
if (keys == null || keys.length != LOOP_COUNT) {
hashMap = new HashMap<>();
hashTable = new Hashtable<>();
treeMap = new TreeMap<>();
linkedHashMap = new LinkedHashMap<>();
String data = "abcdefghijklmnopqrstuvwxyz";
for (int loop = 0; loop < LOOP_COUNT; loop++) {
String tempData = data+loop;
hashMap.put(loop, tempData);
hashTable.put(loop, tempData);
treeMap.put(loop, tempData);
linkedHashMap.put(loop, tempData);
}
keys = RandomKeyUtil.generateRandomNumberKeysSwap(LOOP_COUNT);
}
}
@Benchmark
public void getSeqHashMap() {
for (int loop = 0; loop < LOOP_COUNT; loop++) {
hashMap.get(loop);
}
}
@Benchmark
public void getRandomHashMap() {
for (int loop = 0; loop < LOOP_COUNT; loop++) {
hashMap.get(keys[loop]);
}
}
@Benchmark
public void getSeqHashtable() {
for (int loop = 0; loop < LOOP_COUNT; loop++) {
hashTable.get(loop);
}
}
@Benchmark
public void getRandomHashtable() {
for (int loop = 0; loop < LOOP_COUNT; loop++) {
hashTable.get(keys[loop]);
}
}
@Benchmark
public void getSeqTreeMap() {
for (int loop = 0; loop < LOOP_COUNT; loop++) {
treeMap.get(loop);
}
}
@Benchmark
public void getRandomTreeMap() {
for (int loop = 0; loop < LOOP_COUNT; loop++) {
treeMap.get(keys[loop]);
}
}
@Benchmark
public void getSeqLinkedHashMap() {
for (int loop = 0; loop < LOOP_COUNT; loop++) {
linkedHashMap.get(loop);
}
}
@Benchmark
public void getRandomLinkedHashMap() {
for (int loop = 0; loop < LOOP_COUNT; loop++) {
linkedHashMap.get(keys[loop]);
}
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(MapGet.class.getSimpleName())
.warmupIterations(3)
.measurementIterations(5)
.forks(1)
.build();
new Runner(opt).run();
}
}
# Run complete. Total time: 00:01:08
Benchmark Mode Cnt Score Error Units
MapGet.getRandomHashMap avgt 5 7.069 ± 0.735 us/op
MapGet.getRandomHashtable avgt 5 28.774 ± 2.440 us/op
MapGet.getRandomLinkedHashMap avgt 5 8.615 ± 2.757 us/op
MapGet.getRandomTreeMap avgt 5 72.531 ± 3.146 us/op
MapGet.getSeqHashMap avgt 5 7.663 ± 3.393 us/op
MapGet.getSeqHashtable avgt 5 28.862 ± 11.409 us/op
MapGet.getSeqLinkedHashMap avgt 5 5.933 ± 0.450 us/op
MapGet.getSeqTreeMap avgt 5 55.169 ± 8.445 us/op
트리 형태로 처리하는 TreeMap 클래스가 가장 느린 것을 알 수 있다. 그리고 동기화처리를 하 Hashtable도 HashMap과 속도차이를 보인다.
지금까지 Set, List, Map 관련 클래스에 어떤 것들이 있고, 각각의 성능이 얼마나 되는지 정확하게 측정해 보았다. 하지만 일반적인 웹을 개발할때는 Collection 성능 차이를 비교하는 것은 큰 의미가 없다. 각 클래스에는 사용 목적이 있기 때문에 목적에 부합하는 클래스를 선택해서 사용하는 것이 바람직하다.
Last updated