[java] Java에서 맵 값을 증가시키는 가장 효율적인 방법

이 질문이이 포럼에서 너무 기본적이지 않기를 바랍니다. 그러나 우리는 보게 될 것입니다. 여러 번 실행되는 더 나은 성능을 위해 일부 코드를 리팩터링하는 방법이 궁금합니다.

Map (아마 HashMap)을 사용하여 단어 빈도 목록을 작성한다고 가정하십시오. 여기서 각 키는 계산되는 단어가있는 문자열이고 값은 단어의 토큰을 찾을 때마다 증가하는 정수입니다.

Perl에서 그러한 값을 증가시키는 것은 사소한 일입니다.

$map{$word}++;

그러나 Java에서는 훨씬 더 복잡합니다. 여기 내가 현재하고있는 방식 :

int count = map.containsKey(word) ? map.get(word) : 0;
map.put(word, count + 1);

최신 Java 버전의 오토 박싱 기능에 의존하는 것은 물론입니다. 그러한 가치를 높이는보다 효율적인 방법을 제안 할 수 있는지 궁금합니다. Collections 프레임 워크를 피하고 대신 다른 것을 사용하는 좋은 성능 이유가 있습니까?

업데이트 : 몇 가지 답변을 테스트했습니다. 아래를 참조하십시오.



답변

일부 테스트 결과

나는이 질문에 많은 좋은 답변을 얻었습니다. 감사합니다. 그래서 몇 가지 테스트를 실행하고 실제로 가장 빠른 방법을 결정하기로 결정했습니다. 내가 테스트 한 5 가지 방법은 다음과 같습니다.

  • 질문에 제시 한 “ContainsKey”방법
  • Aleksandar Dimitrov가 제안한 “TestForNull”메소드
  • Hank Gay가 제안한 “AtomicLong”방법
  • jrudolph가 제안한 “Trove”방법
  • phax.myopenid.com에서 제안한 “MutableInt”방법

방법

여기 내가 한 일이 있습니다 …

  1. 아래 표시된 차이점을 제외하고 동일한 5 개의 클래스를 작성했습니다. 각 클래스는 내가 제시 한 시나리오, 즉 10MB 파일을 열고 읽은 다음 파일의 모든 단어 토큰의 빈도 수를 수행하는 일반적인 작업을 수행해야했습니다. 이 작업에는 평균 3 초 밖에 걸리지 않았으므로 I / O가 아닌 주파수 카운트를 10 회 수행했습니다.
  2. 10 회 반복을 반복했지만 I / O 작업은 수행하지 않았 으며 Java Cookbook에서 Ian Darwin의 방법을 사용하여 취한 총 시간 (시계 초)을 기록했습니다 .
  3. 다섯 가지 테스트를 모두 연속으로 수행 한 다음이 작업을 세 번 더 수행했습니다.
  4. 각 방법에 대해 4 개의 결과를 평균했습니다.

결과

먼저 관심있는 사람들을 위해 결과와 아래 코드를 제시하겠습니다.

ContainsKey의 나는 그 방법의 속도에 비해 각 방법의 속도를 줄 것이다, 그래서 방법은, 가장 느린을 예상한다.

  • ContainsKey : 30.654 초 (기준)
  • 원자 길이 : 29.780 초 (1.03 배 빠른 속도)
  • TestForNull : 28.804 초 (1.06 배 빠른 속도)
  • 로브 : 26.313 초 (1.16 배 빠른 속도)
  • MutableInt : 25.747 초 (1.19 배 빠른 속도)

결론

MutableInt 방법과 Trove 방법 만 10 % 이상의 성능 향상을 제공한다는 점에서 훨씬 빠릅니다. 그러나 스레딩이 문제인 경우 AtomicLong이 다른 것보다 매력적일 수 있습니다 (확실하지는 않습니다). final변수로 TestForNull을 실행 했지만 그 차이는 무시할 만했습니다.

다른 시나리오에서 메모리 사용량을 프로파일 링하지 않았습니다. MutableInt 및 Trove 메서드가 메모리 사용에 어떤 영향을 미칠지에 대한 통찰력이있는 사람이라면 누구나 기뻐할 것입니다.

개인적으로 MutableInt 메서드는 타사 클래스를로드 할 필요가 없으므로 가장 매력적입니다. 따라서 문제를 발견하지 않으면 내가 갈 가능성이 가장 큽니다.

코드

각 방법의 중요한 코드는 다음과 같습니다.

ContainsKey

import java.util.HashMap;
import java.util.Map;
...
Map<String, Integer> freq = new HashMap<String, Integer>();
...
int count = freq.containsKey(word) ? freq.get(word) : 0;
freq.put(word, count + 1);

TestForNull

import java.util.HashMap;
import java.util.Map;
...
Map<String, Integer> freq = new HashMap<String, Integer>();
...
Integer count = freq.get(word);
if (count == null) {
    freq.put(word, 1);
}
else {
    freq.put(word, count + 1);
}

원자 긴

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;
...
final ConcurrentMap<String, AtomicLong> map =
    new ConcurrentHashMap<String, AtomicLong>();
...
map.putIfAbsent(word, new AtomicLong(0));
map.get(word).incrementAndGet();

트 로브

import gnu.trove.TObjectIntHashMap;
...
TObjectIntHashMap<String> freq = new TObjectIntHashMap<String>();
...
freq.adjustOrPutValue(word, 1, 1);

MutableInt

import java.util.HashMap;
import java.util.Map;
...
class MutableInt {
  int value = 1; // note that we start at 1 since we're counting
  public void increment () { ++value;      }
  public int  get ()       { return value; }
}
...
Map<String, MutableInt> freq = new HashMap<String, MutableInt>();
...
MutableInt count = freq.get(word);
if (count == null) {
    freq.put(word, new MutableInt());
}
else {
    count.increment();
}


답변

이제 Java 8을 사용하는 더 짧은 방법이 Map::merge있습니다.

myMap.merge(key, 1, Integer::sum)

그것이하는 일 :

  • 경우 키가 존재하지 않습니다 넣어 1 값으로
  • 그렇지 않으면 1키에 연결된 값으로 합산

자세한 내용은 여기를 참조 하십시오 .


답변

2016 년 약간의 연구 : https://github.com/leventov/java-word-count , 벤치 마크 소스 코드

방법 당 최상의 결과 (작을수록 좋음) :

                 time, ms
kolobokeCompile  18.8
koloboke         19.8
trove            20.8
fastutil         22.7
mutableInt       24.3
atomicInteger    25.3
eclipse          26.9
hashMap          28.0
hppc             33.6
hppcRt           36.5

시간 / 공간 결과 :


답변

Google Guava 는 당신의 친구입니다 …

… 적어도 경우에 따라. 그들은이 멋진 AtomicLongMap 있습니다. 맵에서 오랫동안 가치를 다루고 있기 때문에 특히 좋습니다 .

예 :

AtomicLongMap<String> map = AtomicLongMap.create();
[...]
map.getAndIncrement(word);

값에 1을 더 추가 할 수도 있습니다.

map.getAndAdd(word, 112L); 


답변

@ 행복한 게이

내 자신이 아닌 쓸모없는 의견에 대한 후속 조치로 Trove는 갈 길처럼 보입니다. 어떤 이유로, 당신은 표준 JDK 고수하고 싶었 경우 인 ConcurrentMapAtomicLong는 코드 a를 수있는 작은 비트 좋네요, YMMV하지만.

    final ConcurrentMap<String, AtomicLong> map = new ConcurrentHashMap<String, AtomicLong>();
    map.putIfAbsent("foo", new AtomicLong(0));
    map.get("foo").incrementAndGet();

1대한 값을지도에 그대로 둡니다 foo. 현실적으로, 스레딩에 대한 친근감 증가는이 접근법이 권장하는 전부입니다.


답변

Map<String, Integer> map = new HashMap<>();
String key = "a random key";
int count = map.getOrDefault(key, 0); // ensure count will be one of 0,1,2,3,...
map.put(key, count + 1);

그리고 이것이 간단한 코드로 값을 증가시키는 방법입니다.

이익:

  • 새로운 클래스를 추가하거나 변경 가능한 int의 다른 개념을 사용할 필요가 없습니다.
  • 라이브러리에 의존하지 않음
  • 정확히 무슨 일이 일어나고 있는지 이해하기 쉽습니다 (너무 추상화되지 않음)

단점 :

  • 해시 맵은 get () 및 put ()에 대해 두 번 검색됩니다. 따라서 가장 성능이 좋은 코드는 아닙니다.

이론적으로 get ()을 호출하면 put () 위치를 이미 알고 있으므로 다시 검색 할 필요가 없습니다. 그러나 해시 맵에서 검색하는 데는 일반적으로이 성능 문제를 무시할 수있는 시간이 매우 짧습니다.

그러나 문제에 대해 매우 진지한 경우 완벽 주의자이며 다른 방법은 병합 방법을 사용하는 것입니다. 이는 이전의 코드 스 니펫보다 (이론적으로) 한 번만 맵을 검색 할 때보 다 효율적입니다. 이 코드는 처음에는 명확하지 않으며 짧고 성능이 좋습니다)

map.merge(key, 1, (a,b) -> a+b);

제안 : 대부분의 시간 동안 성능 향상이 아닌 코드 가독성에주의해야합니다. 첫 번째 코드 스 니펫이 이해하기 쉬운 경우 사용하십시오. 그러나 두 번째 벌금을 이해할 수 있다면 갈 수도 있습니다!


답변

이런 종류의 일에 대해서는 항상 Google 컬렉션 라이브러리 를 보는 것이 좋습니다 . 이 경우 멀티 세트 가 트릭을 수행합니다.

Multiset bag = Multisets.newHashMultiset();
String word = "foo";
bag.add(word);
bag.add(word);
System.out.println(bag.count(word)); // Prints 2

키 / 항목 등을 반복하는 맵과 유사한 방법이 있습니다. 내부적으로 구현은 현재을 사용 HashMap<E, AtomicInteger>하므로 권투 비용이 발생하지 않습니다.