[Java] Set 구현 Class 중 가장 빠른 Class는??

GilLog·2021년 5월 18일
0

🙆‍♂️ import 🙇‍♂️

자바 성능 튜닝 이야기[ProgrammingInsight-이상민]



Fastset Class in Set implements Class

Set Interface를 구현한 Class 중 가장 빠른 Class를 직접 테스트해보기 위해 jmh 테스트 코드를 작성해보았다.

Data 저장

package com.tuning.jmh;

import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;

import org.open.jdk.jmh.annotations.BenchmarkMode;
import org.open.jdk.jmh.annotations.GenerateMicroBenchmark;
import org.open.jdk.jmh.annotations.Mode;
import org.open.jdk.jmh.annotations.OutputTimeUnit;
import org.open.jdk.jmh.annotations.Scope;
import org.open.jdk.jmh.annotations.State;

@State(Scope.Thread)
@BenchmarkMode({ Mode.AverageTime })
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public class FastestClassInSet {
    int LOOP_COUNT = 1000;
    Set<String> set;
    String data = "gillogfastestsetimplementaions";

    @GenerateMicroBenchmark
    public void addHashSet() {
        set = new HashSet<String>();
        for(int loop = 0; loop < LOOP_COUNT; loop++) {
            set.add(data+loop);
        }
    }
    
    @GenerateMicroBenchmark
    public void addTreeSet() {
        set = new TreeSet<String>();
        for(int loop = 0; loop < LOOP_COUNT; loop++) {
            set.add(data+loop);
        }
    }

    @GenerateMicroBenchmark
    public void addLinkedHashSet() {
        set = new LinkedHashSet<String>();
        for(int loop = 0; loop < LOOP_COUNT; loop++) {
            set.add(data+loop);
        }
    }
}

위 테스트 코드는 Set Interface를 구현한 HashSet, TreeSet, LinkedHashSet의 Data 저장 과정의 성능 차이를 비교해보기 위한 코드이다.

위 테스트의 결과는 아래와 같다.

TargetResponse Sec(micro)
HashSet375
TreeSet1,214
LinkedHashSet378

HashSetLinkedHashSet의 성능은 비슷한 것으로 파악되었고, TreeSet의 순서 정렬 수행 작업으로 인해서 가장 성능이 뒤쳐지는 것으로 결과가 나왔다.


Data Size를 알고 있다면??

현재 Data의 크기를 알고 있으므로 HashSet에 크기를 지정해서 테스트를 해보면 아래와 같다.

    @GenerateMicroBenchmark
    public void addHashSetWithInitialSize() {
        set = new HashSet<String>(LOOP_COUNT);
        for(int loop = 0; loop < LOOP_COUNT; loop++) {
            set.add(data+loop);
        }
    }
TargetResponse Sec(micro)
HashSet375
HashSetWithIntialSize353

큰 차이는 발생하지 않았지만, Data 크기를 미리 알고 있을 경우 객체 생성시 크기를 미리 지정하는 것이 성능상 유리하다.

Data 출력

이번에는 Set을 구현한 Class들이 Data를 출력할 때 얼마나 많은 차이가 발생하는지 확인해보려 한다.

테스트 코드는 아래와 같다.

package com.tuning.jmh;

import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;

import org.open.jdk.jmh.annotations.BenchmarkMode;
import org.open.jdk.jmh.annotations.GenerateMicroBenchmark;
import org.open.jdk.jmh.annotations.Mode;
import org.open.jdk.jmh.annotations.OutputTimeUnit;
import org.open.jdk.jmh.annotations.Scope;
import org.open.jdk.jmh.annotations.State;

@State(Scope.Thread)
@BenchmarkMode({ Mode.AverageTime })
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public class FastestClassInSet {
    int LOOP_COUNT = 1000;
    Set<String> hashSet;
    Set<String> treeSet;
    Set<String> linkedHashSet;
    String data = "gillogfastestsetimplementaions";
    String []keys;
    String result = null;
    
    @Setup(Level.Trial)
    public void setUp() {
        hashSet = new HashSet<String>();
        treeSet = new TreeSet<String>();
        linkedHashSet = new LinkedHashSet<String>();
        for(int loop=0;loop<LOOP_COUNT;loop++) {
            String tempData = data + loop;
            hashSet.add(tempData);
            treeSet.add(tempData);
            linkedHashSet.add(tempData);
        }
    }

    @GenerateMicroBenchmark
    public void iterateHashSet() {
        Iterator<String> iterator = hashSet.iterator();
        while(iterator.hasNext()) {
            result = iterator.next();
        }
    }
    
    @GenerateMicroBenchmark
    public void iterateTreeSet() {
        Iterator<String> iterator = treeSet.iterator();
        while(iterator.hasNext()) {
            result = iterator.next();
        }
    }

    @GenerateMicroBenchmark
    public void iterateLinkedHashSet() {
        Iterator<String> iterator = linkedHashSet.iterator();
        while(iterator.hasNext()) {
            result = iterator.next();
        }
    }
}

테스트 결과는 아래와 같다.

TargetResponse Sec(micro)
HashSet26
TreeSet35
LinkedHashSet16

LinkedHashSet Class가 가장 빠르게 Data를 읽고, HashSet, TreeSet 순으로 성능 차이가 나는걸 알 수 있다.

일반적으로 Set은 여러 Data를 저장하고 해당 Data가 존재하는지를 확인하는 용도로 가장 많이 사용된다.

따라서 Iterator로 Data를 읽어오는것이 아니라 Random 하게 Data를 가져와야 한다.

이번에는 Random하게 Data를 읽어오는 부분에서 성능 테스트를 진행해보려 한다.

Data를 Random하게 읽어오기 위해 아래와 같은 Class를 생성하였다.

package com.tuning.jmh;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.Random;
import java.util.Set;

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 randomNumberOne = random.nextInt(maxNumber);
            int randomNumberTwo = random.nextInt(maxNumber);
            String temp = result[randomNumberTwo];
            result[randomNumberTwo] = result[randomNumberOne];
            result[randomNumberOne] = temp;
        }
        return result;
    }
}

generateRandomSetKeysSwap method를 통해 Data의 개수 만큼 불규칙적인 key를 가져올 수 있다.

이제 비순차적으로 Data를 읽어와 성능 테스트를 수행하는 테스트 코드는 아래와 같다.

package com.tuning.jmh;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.open.jdk.jmh.annotations.BenchmarkMode;
import org.open.jdk.jmh.annotations.GenerateMicroBenchmark;
import org.open.jdk.jmh.annotations.Mode;
import org.open.jdk.jmh.annotations.OutputTimeUnit;
import org.open.jdk.jmh.annotations.Scope;
import org.open.jdk.jmh.annotations.State;
import com.tuning.jmh.RandomKeyUtil;

@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 = "gillogfastestsetimplementaions";
    String []keys;
    
    @Setup(Level.Trial)
    public void setUp() {
        hashSet = new HashSet<String>();
        treeSet = new TreeSet<String>();
        linkedHashSet = new LinkedHashSet<String>();
        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 = generateRandomSetKeysSwap(hashSet);
        }
    }

    @GenerateMicroBenchmark
    public void containsHashSet() {
        for(String key : keys) {
            hashSet.contains(key);
        }
    }

    @GenerateMicroBenchmark
    public void containsTreeSet() {
        for(String key : keys) {
            treeSet.contains(key);
        }
    }

    @GenerateMicroBenchmark
    public void containsLinkedHashSet() {
        for(String key : keys) {
            linkedHashSet.contains(key);
        }
    }
}

해당 테스트의 결과는 아래와 같다.

TargetResponse Sec(micro)
HashSet32
TreeSet841
LinkedHashSet32

HashSetLinkedHashSet의 속도는 이전 순차적인 Data Reading 테스트 결과와 비슷하게 측정되었지만, TreeSet의 속도는 현저히 느려진걸 확인할 수 있다.

이는 TreeSet Class는 Data를 저장하면서 정렬을 수행한다.

TreeSet의 선언문을 보면 아래와 같은데,

public class TreeSet<E> extends AbstractSet<E>
	implements NavigableSet<E>, Cloneable, Serializable

구현 Interface 중 NavigableSet Interface가 있다.

NavigableSet Interface는 특정 값 보다 큰 값이나 작은 값, 가장 큰 값, 가장 작은 값 등을 추출하는 method를 선언해 놓았고, JDK 1.6 부터 추가된 Interface이다.

이로인해 TreeSet은 Data를 순서에 따라 탐색하는 작업이 필요한 경우 사용하는 것이 좋다는걸 확인할 수 있다.

하지만 순서에 따른 탐색 작업이 필요 없을 경우 HashSet이나 LinkedHashSet을 사용하는 것이 성능상 더 빠르다.

profile
🚀 기록보단 길록을 20.10 ~ 22.02 ⭐ Move To : https://gil-log.github.io/

0개의 댓글