development

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

big-blog 2020. 3. 2. 13:11
반응형

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"메소드
  • 행크 게이가 제안한 "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(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. 현실적으로, 스레딩에 대한 친근감 향상은이 접근법이 권장하는 전부입니다.


이런 종류의 일에 대해서는 항상 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>하므로 권투 비용이 발생하지 않습니다.


당신은 당신의 원래 시도 사실을 알고 있어야

int count = map.containsKey (word)? map.get (word) : 0;

지도에 잠재적으로 비싼 두 가지 작업, 즉 containsKey및을 포함 get합니다. 전자는 후자와 거의 비슷한 작업을 수행하므로 동일한 작업을 두 번 수행합니다 !

API for Map을 보면 get일반적으로 null맵에 요청 된 요소가 포함되지 않은 경우 오퍼레이션이 리턴 됩니다.

이것은 같은 해결책을 만들 것입니다

map.put (키, map.get (key) + 1);

NullPointerExceptions를 산출 할 수 있으므로 위험합니다 . null먼저 확인해야합니다 .

또한주의 , 이것은 것이 매우 중요 HashMap수 있습니다 포함 nulls정의. 따라서 모든 반환 된 null"그런 요소가 없다"고 말하는 것은 아닙니다. 이러한 측면에서, containsKey동작 다르게 에서 get실제로 말의 여부 등의 요소가있다. 자세한 내용은 API를 참조하십시오.

그러나 귀하의 경우, 저장된 null것과 "noSuchElement" 를 구별하고 싶지 않을 수 있습니다 . 를 허용하지 않으려면을 null선호 할 수 있습니다 Hashtable. 다른 답변에서 이미 제안 된 래퍼 라이브러리를 사용하면 응용 프로그램의 복잡성에 따라 수동 처리에 더 나은 솔루션이 될 수 있습니다.

, 기본적으로 그 일을하는 가장 좋은 방법을 답을 완료 (내가 편집 기능, 처음에 덕분에 그것을 넣어 잊었다!)하기 위해하는 것입니다 getfinal대한 변수를 확인 null하고 put는 뒤쪽에와 1. final어쨌든 불변이기 때문에 변수가되어야 합니다. 컴파일러는이 힌트가 필요하지 않을 수도 있지만 그렇게 명확합니다.

최종 해시 맵 맵 = generateRandomHashMap ();
최종 객체 키 = fetchSomeKey ();
최종 정수 i = map.get (key);
if (i! = null) {
    map.put (i + 1);
} else {
    // 무언가를한다
}

오토 박싱에 의존하고 싶지 않다면, map.put(new Integer(1 + i.getValue()));대신 비슷한 말을해야 합니다.


Map<String, Integer> map = new HashMap<>();
String key = "a random key";
int count = map.getOrDefault(key, 0);
map.put(key, count + 1);

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

이익:

  • 변경 가능한 int에 대한 다른 클래스를 만들지 않음
  • 짧은 코드
  • 이해하기 쉬운
  • 널 포인터 예외 없음

다른 방법은 병합 방법을 사용하는 것이지만 값을 늘리기에는 너무 많습니다.

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

제안 : 대부분의 시간 동안 성능 향상이 아닌 코드 가독성에주의해야합니다.


또 다른 방법은 가변 정수를 만드는 것입니다.

class MutableInt {
  int value = 0;
  public void inc () { ++value; }
  public int get () { return value; }
}
...
Map<String,MutableInt> map = new HashMap<String,MutableInt> ();
MutableInt value = map.get (key);
if (value == null) {
  value = new MutableInt ();
  map.put (key, value);
} else {
  value.inc ();
}

물론 이것은 추가 객체를 만드는 것을 의미하지만 Integer를 만드는 것과 비교할 때 오버 헤드 (Integer.valueOf로도)는 그리 많지 않아야합니다.


Java 8 에서 제공되는 인터페이스 에서 computeIfAbsent 메소드를 사용할 수 있습니다 .Map

final Map<String,AtomicLong> map = new ConcurrentHashMap<>();
map.computeIfAbsent("A", k->new AtomicLong(0)).incrementAndGet();
map.computeIfAbsent("B", k->new AtomicLong(0)).incrementAndGet();
map.computeIfAbsent("A", k->new AtomicLong(0)).incrementAndGet(); //[A=2, B=1]

이 방법 computeIfAbsent은 지정된 키가 이미 값과 연결되어 있는지 확인합니까? 연관된 값이 없으면 주어진 맵핑 함수를 사용하여 값을 계산하려고 시도합니다. 어쨌든 지정된 키와 관련된 현재 (기존 또는 계산 된) 값을 반환하거나 계산 된 값이 null 인 경우 null을 반환합니다.

참고로 여러 스레드가 공통 합계를 업데이트하는 상황이 발생하는 경우 LongAdder 클래스를 살펴볼 수 있습니다. 높은 경합에서이 클래스의 예상 처리량은 AtomicLong공간 소비를 높이기 위해 보다 훨씬 높습니다 .


int의 128보다 크거나 같은 모든 boxing이 객체 할당을 유발하기 때문에 메모리 회전이 문제가 될 수 있습니다 (Integer.valueOf (int) 참조). 가비지 수집기는 수명이 짧은 개체를 매우 효율적으로 처리하지만 성능은 어느 정도 저하됩니다.

증가 횟수가 키 수 (=이 경우 단어 수)를 크게 초과한다는 것을 알고 있다면, 대신 int holder를 사용하십시오. Phax는 이미 이에 대한 코드를 제시했습니다. 다시 두 가지 변경 사항이 있습니다 (홀더 클래스는 정적 및 초기 값을 1로 설정).

static class MutableInt {
  int value = 1;
  void inc() { ++value; }
  int get() { return value; }
}
...
Map<String,MutableInt> map = new HashMap<String,MutableInt>();
MutableInt value = map.get(key);
if (value == null) {
  value = new MutableInt();
  map.put(key, value);
} else {
  value.inc();
}

최고의 성능이 필요한 경우 기본 값 유형에 직접 맞게 조정 된 Map 구현을 찾으십시오. jrudolph는 GNU Trove를 언급했습니다 .

그건 그렇고,이 주제에 대한 좋은 검색어는 "히스토그램"입니다.


containsKey ()를 호출하는 대신 map.get을 호출하고 반환 된 값이 null인지 아닌지를 확인하는 것이 더 빠릅니다.

    Integer count = map.get(word);
    if(count == null){
        count = 0;
    }
    map.put(word, count + 1);

이것이 병목임을 확신합니까? 성능 분석을 수행 했습니까?

NetBeans 프로파일 러 (무료 및 NB 6.1에 내장)를 사용하여 핫스팟을보십시오.

마지막으로 JVM 업그레이드 (예 : 1.5-> 1.6)는 종종 저렴한 성능 향상 도구입니다. 빌드 번호를 업그레이드하더라도 성능이 향상 될 수 있습니다. Windows에서 실행 중이고 이것이 서버 클래스 응용 프로그램 인 경우 명령 행에서 -server를 사용하여 Server Hotspot JVM을 사용하십시오. Linux 및 Solaris 시스템에서는 자동 감지됩니다.


몇 가지 접근 방식이 있습니다.

  1. Google 컬렉션에 포함 된 세트와 같은 백 alorithm을 사용하십시오.

  2. 맵에서 사용할 수있는 가변 컨테이너를 작성하십시오.


    class My{
        String word;
        int count;
    }

그리고 put ( "word", new My ( "Word")); 그런 다음 존재하는지 확인하고 추가 할 때 증가시킬 수 있습니다.

innerloop 검색 및 정렬을 수행하면 성능이 저하되므로 목록을 사용하여 자체 솔루션을 롤링하지 마십시오. 첫 번째 HashMap 솔루션은 실제로 매우 빠르지 만 Google 컬렉션에있는 것과 비슷한 것이 더 좋습니다.

Google 컬렉션을 사용하여 단어를 세면 다음과 같습니다.



    HashMultiset s = new HashMultiset();
    s.add("word");
    s.add("word");
    System.out.println(""+s.count("word") );


백 알고리즘은 단어를 계산할 때 필요한 것이므로 HashMultiset을 사용하는 것은 상당히 일리가 없습니다.


귀하의 솔루션이 표준 방법이라고 생각하지만 직접 언급했듯이 아마도 가장 빠른 방법은 아닙니다.

GNU Trove를 볼 수 있습니다 . 그것은 모든 종류의 빠른 기본 컬렉션을 포함하는 라이브러리입니다. 귀하의 예제 는 원하는 것을 정확하게 수행하는 adjustOrPutValue 메소드가 있는 TObjectIntHashMap사용합니다 .


약간의 해킹이 발생할 경우 MutableInt 접근 방식의 변형은 단일 요소 int 배열을 사용하는 것입니다.

Map<String,int[]> map = new HashMap<String,int[]>();
...
int[] value = map.get(key);
if (value == null) 
  map.put(key, new int[]{1} );
else
  ++value[0];

이 변형으로 성능 테스트를 다시 실행할 수 있다면 흥미로울 것입니다. 가장 빠를 수도 있습니다.


편집 : 위의 패턴은 나에게 잘 작동했지만 결국 Trove의 컬렉션을 사용하여 내가 만든 매우 큰지도에서 메모리 크기를 줄 이도록 변경했으며 보너스로 더 빨랐습니다.

정말 좋은 기능 중 하나는 TObjectIntHashMap클래스에 단일 adjustOrPutValue호출이 있다는 것입니다. 해당 키에 이미 값이 있는지 여부에 따라 초기 값을 넣거나 기존 값을 증가시킵니다. 이것은 증분에 적합합니다.

TObjectIntHashMap<String> map = new TObjectIntHashMap<String>();
...
map.adjustOrPutValue(key, 1, 1);

Google Collections HashMultiset :
-사용하기 매우 우아
하지만 CPU 및 메모리 소비

가장 좋은 방법은 다음과 같습니다. Entry<K,V> getOrPut(K);(우아하고 저렴한 비용)

이러한 메소드는 해시와 색인을 한 번만 계산 한 다음 항목으로 원하는 작업을 수행 할 수 있습니다 (값 바꾸기 또는 업데이트).

더 우아한 :
- 필요한 경우 새 항목 HashSet<Entry>
get(K)넣을 수 있도록 확장하십시오
-항목은 자신의 객체가 될 수 있습니다.
->(new MyHashSet()).get(k).increment();


"put"에는 "get"이 필요합니다 (중복 키가 없도록).
따라서 "put"을 직접 수행하고
이전 값이 있으면 추가를 수행하십시오.

Map map = new HashMap ();

MutableInt newValue = new MutableInt (1); // default = inc
MutableInt oldValue = map.put (key, newValue);
if (oldValue != null) {
  newValue.add(oldValue); // old + inc
}

카운트가 0에서 시작하면 1을 추가하십시오 (또는 다른 값은 ...).

Map map = new HashMap ();

MutableInt newValue = new MutableInt (0); // default
MutableInt oldValue = map.put (key, newValue);
if (oldValue != null) {
  newValue.setValue(oldValue + 1); // old + inc
}

주의 : 이 코드는 스레드 안전하지 않습니다. 맵을 빌드 한 다음 동시에 업데이트하지 않고 맵을 사용하는 데 사용하십시오.

최적화 : 루프에서 이전 값을 유지하여 다음 루프의 새로운 값이됩니다.

Map map = new HashMap ();
final int defaut = 0;
final int inc = 1;

MutableInt oldValue = new MutableInt (default);
while(true) {
  MutableInt newValue = oldValue;

  oldValue = map.put (key, newValue); // insert or...
  if (oldValue != null) {
    newValue.setValue(oldValue + inc); // ...update

    oldValue.setValue(default); // reuse
  } else
    oldValue = new MutableInt (default); // renew
  }
}

아주 간단하게 Map.java다음 같이 내장 함수를 사용하십시오.

map.put(key, map.getOrDefault(key, 0) + 1);

예를 들어, 다양한 프리미티브 래퍼 Integer는 변경할 수 없으므로 AtomicLong 과 같은 방법으로 수행 할 수 없다면 요청하는 것을 수행하는 더 간결한 방법은 없습니다 . 나는 그것을 잠시 후에 줄 수 있고 업데이트 할 수 있습니다. BTW, Hashtable Collections Framework 의 일부입니다 .


Apache Collections Lazy Map (값을 0으로 초기화)을 사용하고 Apache Lang의 MutableIntegers를 해당 맵의 값으로 사용합니다.

가장 큰 비용은 메소드에서 맵을 두 번 처리해야합니다. 내 경우에는 한 번만하면됩니다. 값을 얻고 (없는 경우 초기화됩니다) 값을 늘리십시오.


기능 자바 라이브러리의 TreeMap자료 구조는이 update최신 트렁크 헤드 방법 :

public TreeMap<K, V> update(final K k, final F<V, V> f)

사용법 예 :

import static fj.data.TreeMap.empty;
import static fj.function.Integers.add;
import static fj.pre.Ord.stringOrd;
import fj.data.TreeMap;

public class TreeMap_Update
  {public static void main(String[] a)
    {TreeMap<String, Integer> map = empty(stringOrd);
     map = map.set("foo", 1);
     map = map.update("foo", add.f(1));
     System.out.println(map.get("foo").some());}}

이 프로그램은 "2"를 인쇄합니다.


@ Vilmantas Baranauskas :이 답변과 관련하여 담당자가 있다면 의견을 말하지만 그렇지 않습니다. value ()를 동기화하지 않고 inc ()를 동기화하는 것만으로는 충분하지 않기 때문에 스레드로부터 안전하지 않다고 정의한 Counter 클래스에 주목하고 싶습니다. value ()를 호출하는 다른 스레드는 업데이트와 관계가 설정되어 있지 않으면 값을 볼 수 없습니다.


나는 그것이 얼마나 효율적인지 모르지만 아래 코드도 잘 작동합니다 BiFunction. 처음 에는를 정의해야합니다 . 또한이 방법으로 증분 이상의 것을 만들 수 있습니다.

public static Map<String, Integer> strInt = new HashMap<String, Integer>();

public static void main(String[] args) {
    BiFunction<Integer, Integer, Integer> bi = (x,y) -> {
        if(x == null)
            return y;
        return x+y;
    };
    strInt.put("abc", 0);


    strInt.merge("abc", 1, bi);
    strInt.merge("abc", 1, bi);
    strInt.merge("abc", 1, bi);
    strInt.merge("abcd", 1, bi);

    System.out.println(strInt.get("abc"));
    System.out.println(strInt.get("abcd"));
}

출력은

3
1

Eclipse Collections를 사용하는 경우을 사용할 수 있습니다 HashBag. 메모리 사용 측면에서 가장 효율적인 접근 방식이며 실행 속도 측면에서도 잘 수행됩니다.

HashBag객체 MutableObjectIntMap대신 기본 정수를 저장 하는에 의해 뒷받침됩니다 Counter. 이것은 메모리 오버 헤드를 줄이고 실행 속도를 향상시킵니다.

HashBagCollection항목의 발생 횟수를 쿼리 할 수 있는 API이기 때문에 필요한 API를 제공합니다 .

다음은 Eclipse Collections Kata 의 예입니다 .

MutableBag<String> bag =
  HashBag.newBagWith("one", "two", "two", "three", "three", "three");

Assert.assertEquals(3, bag.occurrencesOf("three"));

bag.add("one");
Assert.assertEquals(2, bag.occurrencesOf("one"));

bag.addOccurrences("one", 4);
Assert.assertEquals(6, bag.occurrencesOf("one"));

참고 : 저는 Eclipse Collections의 커미터입니다.


Java 8 Map :: compute ()를 사용하는 것이 좋습니다. 키가 존재하지 않는 경우도 고려합니다.

Map.compute(num, (k, v) -> (v == null) ? 1 : v + 1);

많은 사람들이 Groovy 답변에 대한 Java 주제를 검색하므로 Groovy에서 수행 할 수있는 방법은 다음과 같습니다.

dev map = new HashMap<String, Integer>()
map.put("key1", 3)

map.merge("key1", 1) {a, b -> a + b}
map.merge("key2", 1) {a, b -> a + b}

나는 당신의 질문을 올바르게 이해하고 있기를 바랍니다. 나는 파이썬에서 Java로 왔습니다. 그래서 나는 당신의 투쟁에 공감할 수 있습니다.

당신이 가지고 있다면

map.put(key, 1)

당신은 할 것

map.put(key, map.get(key) + 1)

도움이 되었기를 바랍니다!


Java 8에서 간단하고 쉬운 방법은 다음과 같습니다.

final ConcurrentMap<String, AtomicLong> map = new ConcurrentHashMap<String, AtomicLong>();
    map.computeIfAbsent("foo", key -> new AtomicLong(0)).incrementAndGet();

참고 URL : https://stackoverflow.com/questions/81346/most-efficient-way-to-increment-a-map-value-in-java



반응형