[java] Java HashMap 성능 최적화 / 대안

큰 HashMap을 만들고 싶지만 put()성능이 충분하지 않습니다. 어떤 아이디어?

다른 데이터 구조 제안은 환영하지만 Java Map의 조회 기능이 필요합니다.

map.get(key)

제 경우에는 2,600 만 개의 항목이있는지도를 만들고 싶습니다. 표준 Java HashMap을 사용하면 2 ~ 3 백만 번의 삽입 후 넣기 속도가 견딜 수 없을 정도로 느려집니다.

또한 키에 대해 다른 해시 코드 배포를 사용하는 것이 도움이 될 수 있는지 아는 사람이 있습니까?

내 해시 코드 방법 :

byte[] a = new byte[2];
byte[] b = new byte[3];
...

public int hashCode() {
    int hash = 503;
    hash = hash * 5381 + (a[0] + a[1]);
    hash = hash * 5381 + (b[0] + b[1] + b[2]);
    return hash;
}

동일한 객체가 동일한 해시 코드를 갖도록하기 위해 더하기의 연관 속성을 사용하고 있습니다. 배열은 0-51 범위의 값을 가진 바이트입니다. 값은 두 배열에서 한 번만 사용됩니다. a 배열에 동일한 값 (어느 순서로든)이 포함되어 있고 b 배열에 대해서도 동일한 경우 객체는 동일합니다. 따라서 a = {0,1} b = {45,12,33} 및 a = {1,0} b = {33,45,12}는 같습니다.

편집, 몇 가지 참고 :

  • 몇몇 사람들은 2,600 만 개의 항목을 저장하기 위해 해시 맵 또는 기타 데이터 구조를 사용하여 비판했습니다. 왜 이것이 이상하게 보일지 모르겠습니다. 나에게는 고전적인 데이터 구조 및 알고리즘 문제처럼 보입니다. 2 천 6 백만 개의 항목이 있고이를 데이터 구조에 빠르게 삽입하고 조회 할 수 있기를 원합니다. 데이터 구조와 알고리즘을 제공합니다.

  • 기본 Java HashMap의 초기 용량을 2,600만으로 설정 하면 성능이 저하 됩니다.

  • 어떤 사람들은 확실히 현명한 선택 인 다른 상황에서 데이터베이스 사용을 제안했습니다. 그러나 나는 정말로 데이터 구조와 알고리즘에 대한 질문을하고 있는데, 전체 데이터베이스는 좋은 데이터 구조 솔루션보다 과도하고 훨씬 더 느릴 것입니다.



답변

많은 사람들이 지적했듯이 그 hashCode()방법은 비난입니다. 2,600 만 개의 개별 개체에 대해 약 20,000 개의 코드 만 생성했습니다. 이는 해시 버킷 당 평균 1,300 개의 개체 = 매우 나쁩니다. 그러나 두 배열을 기본 52의 숫자로 바꾸면 모든 객체에 대해 고유 한 해시 코드를 얻을 수 있습니다.

public int hashCode() {
    // assume that both a and b are sorted       
    return a[0] + powerOf52(a[1], 1) + powerOf52(b[0], 2) + powerOf52(b[1], 3) + powerOf52(b[2], 4);
}

public static int powerOf52(byte b, int power) {
    int result = b;
    for (int i = 0; i < power; i++) {
        result *= 52;
    }
    return result;
}

배열은이 메서드 hashCode()가 동일한 객체가 동일한 해시 코드를 갖는 계약을 이행하도록 정렬됩니다 . 이전 방법을 사용하면 100,000 풋 블록, 100,000에서 2,000,000 사이의 초당 평균 풋 수는 다음과 같습니다.

168350.17
109409.195
81344.91
64319.023
53780.79
45931.258
39680.29
34972.676
31354.514
28343.062
25562.371
23850.695
22299.22
20998.006
19797.799
18702.951
17702.434
16832.182
16084.52
15353.083

새로운 방법을 사용하면 다음이 제공됩니다.

337837.84
337268.12
337078.66
336983.97
313873.2
317460.3
317748.5
320000.0
309704.06
310752.03
312944.5
265780.75
275540.5
264350.44
273522.97
270910.94
279008.7
276285.5
283455.16
289603.25

훨씬 낫습니다. 새로운 방법은 좋은 처리량을 유지하는 동안 이전 방법은 매우 빠르게 끝났습니다.


답변

나는 당신에 통지 한 가지 hashCode()방법은 배열에있는 요소의 순서이다 a[]와는 b[]상관하지 않습니다. 따라서 (a[]={1,2,3}, b[]={99,100})와 동일한 값으로 해시됩니다 (a[]={3,1,2}, b[]={100,99}). 실제로 모든 키 k1k2위치 sum(k1.a)==sum(k2.a)sum(k1.b)=sum(k2.b)충돌이 발생합니다. 배열의 각 위치에 가중치를 할당하는 것이 좋습니다.

hash = hash * 5381 + (c0*a[0] + c1*a[1]);
hash = hash * 5381 + (c0*b[0] + c1*b[1] + c3*b[2]);

여기서 c0, c1하고 c3있는 별개의 상수 (당신이 다른 상수를 사용할 수 있습니다 b필요한 경우). 그것은 일을 조금 더 균등하게해야합니다.


답변

Pascal에 대해 자세히 설명하려면 : HashMap이 어떻게 작동하는지 이해하십니까? 해시 테이블에 몇 개의 슬롯이 있습니다. 각 키에 대한 해시 값을 찾은 다음 테이블의 항목에 매핑합니다. 두 개의 해시 값이 동일한 항목 ( “해시 충돌”)에 매핑되면 HashMap은 연결 목록을 만듭니다.

해시 충돌은 해시 맵의 성능을 저하시킬 수 있습니다. 극단적 인 경우 모든 키에 동일한 해시 코드가 있거나 다른 해시 코드가 있지만 모두 동일한 슬롯에 매핑되면 해시 맵이 연결된 목록으로 바뀝니다.

따라서 성능 문제가 발생하는 경우 가장 먼저 확인해야 할 것은 해시 코드의 무작위 분포를 얻고 있는가? 그렇지 않다면 더 나은 해시 함수가 필요합니다. 이 경우 “더 좋음”은 “내 특정 데이터 세트에 더 좋음”을 의미 할 수 있습니다. 예를 들어, 문자열로 작업하고 해시 값으로 문자열 길이를 취했다고 가정합니다. (Java의 String.hashCode가 작동하는 방식은 아니지만 간단한 예제를 작성하는 것입니다.) 문자열의 길이가 1에서 10,000까지 다양하고 해당 범위에 걸쳐 상당히 균등하게 분산되어 있다면 이것은 매우 좋을 수 있습니다. 해시 함수. 그러나 문자열이 모두 1 자 또는 2 자이면 이것은 매우 나쁜 해시 함수입니다.

편집 : 추가해야합니다 : 새 항목을 추가 할 때마다 HashMap은 이것이 중복인지 확인합니다. 해시 충돌이 발생하면 들어오는 키를 해당 슬롯에 매핑 된 모든 키와 비교해야합니다. 따라서 모든 것이 단일 슬롯에 해시되는 최악의 경우 두 번째 키는 첫 번째 키와 비교되고 세 번째 키는 # 1 및 # 2와 비교되고 네 번째 키는 # 1, # 2 및 # 3과 비교됩니다. 등. 키 # 1 백만에 도달 할 때까지 1 조 건 이상의 비교를 수행했습니다.

@Oscar : 음, 그게 “진짜가 아님”인지 모르겠어요. 좀 더 “명확하게 해줘”와 비슷합니다. 그러나 예, 기존 항목과 동일한 키로 새 항목을 만들면 첫 번째 항목을 덮어 쓰는 것이 사실입니다. 이것이 제가 마지막 단락에서 중복을 찾는 것에 대해 이야기했을 때 의미 한 것입니다. 키가 같은 슬롯에 해시 될 때마다 HashMap은 그것이 기존 키의 중복인지 또는 우연히 같은 슬롯에 있는지 확인해야합니다. 해시 함수. 이것이 HashMap의 “전체 지점”이라는 것을 모르겠습니다. “전체 지점”은 키로 요소를 빠르게 검색 할 수 있다는 것입니다.

하지만 어쨌든, 그것은 제가 만들려고했던 “전체 지점”에 영향을주지 않습니다. 두 개의 키가있을 때-예, 다른 키가 다시 표시되지 않고-테이블의 동일한 슬롯에 매핑됩니다. , HashMap은 연결 목록을 작성합니다. 그런 다음 각 새 키가 실제로 기존 키의 중복인지 확인해야하기 때문에이 동일한 슬롯에 매핑되는 새 항목을 추가하려는 각 시도는 연결된 목록을 추적하여 기존 항목이 있는지 확인해야합니다. 이전에 본 키의 복제본이거나 새 키인 경우

원래 게시물 이후 오래 업데이트

나는 게시물을 올린 지 6 년 만에이 답변에 대한 찬성 투표를 받았고이 질문을 다시 읽게되었습니다.

질문에 제공된 해시 함수는 2,600 만 항목에 대한 좋은 해시가 아닙니다.

a [0] + a [1]과 b [0] + b [1] + b [2]를 더합니다. 그는 각 바이트의 값이 0에서 51까지 범위가되므로 (51 * 2 + 1) * (51 * 3 + 1) = 15,862 개의 가능한 해시 값만 제공합니다. 2,600 만 개의 항목이 있다는 것은 해시 값당 평균 약 1639 개의 항목을 의미합니다. 그것은 많은 충돌이며, 연결된 목록을 통해 많은 순차 검색이 필요합니다.

OP는 배열 a와 배열 b 내의 다른 차수가 동일한 것으로 간주되어야한다고 말합니다. 즉, [[1,2], [3,4,5]]. equals ([[2,1], [5,3,4] ]), 따라서 계약을 이행하려면 동일한 해시 코드가 있어야합니다. 괜찮아. 여전히 15,000 개 이상의 가능한 값이 있습니다. 그의 두 번째 제안 된 해시 함수는 훨씬 더 우수하여 더 넓은 범위를 제공합니다.

다른 사람이 언급했듯이 해시 함수가 다른 데이터를 변경하는 것은 부적절 해 보입니다. 객체가 생성 될 때 객체를 “정규화”하거나 배열 사본에서 해시 함수가 작동하도록하는 것이 더 합리적입니다. 또한 루프를 사용하여 함수를 통해 매번 상수를 계산하는 것은 비효율적입니다. 여기에 4 개의 값만 있기 때문에

return a[0]+a[1]*52+b[0]*52*52+b[1]*52*52*52+b[2]*52*52*52*52;

이것은 컴파일러가 컴파일 타임에 한 번 계산을 수행하게합니다. 또는 클래스에 4 개의 정적 상수가 정의되어 있습니다.

또한 해시 함수의 첫 번째 초안에는 출력 범위에 추가 할 작업이없는 몇 가지 계산이 있습니다. 그는 먼저 클래스의 값을 고려하기 전에 5381을 곱하는 것보다 먼저 해시 = 503을 설정합니다. 그래서 … 사실상 그는 모든 값에 503 * 5381을 더합니다. 이것은 무엇을 성취합니까? 모든 해시 값에 상수를 추가하면 유용한 작업을 수행하지 않고 CPU 주기만 소모됩니다. 여기서 교훈 : 해시 함수에 복잡성을 추가하는 것은 목표가 아닙니다. 목표는 복잡성을 위해 복잡성을 추가하는 것이 아니라 다양한 가치를 얻는 것입니다.


답변

내 첫 번째 아이디어는 HashMap을 적절하게 초기화하고 있는지 확인하는 것입니다. 로부터 HashMap에 대한 JavaDoc을 :

HashMap의 인스턴스에는 성능에 영향을 미치는 두 가지 매개 변수, 즉 초기 용량과로드 비율이 있습니다. 용량은 해시 테이블의 버킷 수이고 초기 용량은 단순히 해시 테이블이 생성 된 시점의 용량입니다. 로드 팩터는 용량이 자동으로 증가하기 전에 해시 테이블이 얼마나 꽉 찬지 측정합니다. 해시 테이블의 항목 수가 부하 계수와 현재 용량의 곱을 초과하면 해시 테이블이 약 두 배의 버킷 수를 갖도록 해시 테이블이 다시 해시됩니다 (즉, 내부 데이터 구조가 다시 작성 됨).

따라서 너무 작은 HashMap으로 시작하는 경우 크기를 조정해야 할 때마다 모든 해시가 다시 계산됩니다. 이는 2-3 백만 개의 삽입 지점에 도달했을 때 느끼는 것일 수 있습니다.


답변

세 가지 접근 방식을 제안합니다.

  1. 더 많은 메모리로 Java 실행 : java -Xmx256M예를 들어 256MB로 실행합니다. 필요한 경우 더 많이 사용하고 RAM이 많이 있습니다.

  2. 다른 포스터에서 제안한대로 계산 된 해시 값을 캐시하여 각 개체가 해시 값을 한 번만 계산하도록합니다.

  3. 더 나은 해싱 알고리즘을 사용하십시오. 게시 한 것은 a = {0, 1} 인 경우 a = {1, 0} 인 경우와 동일한 해시를 반환하고 나머지는 모두 동일합니다.

Java가 무료로 제공하는 것을 활용하십시오.

public int hashCode() {
    return 31 * Arrays.hashCode(a) + Arrays.hashCode(b);
}

데이터의 정확한 특성에 따라 다르지만 기존 hashCode 메서드보다 충돌 가능성이 훨씬 적다고 확신합니다.


답변

“on / off topic”의 회색 영역으로 들어가지만 Oscar Reyes의 제안과 관련하여 혼란을 없애기 위해 필요합니다. 더 많은 해시 충돌이 HashMap의 요소 수를 줄이므로 좋은 것입니다. 오스카가 무슨 말을하는지 오해 할 수도 있지만, kdgregory, delfuego, Nash0, 그리고 나는 모두 같은 (오해) 이해를 공유하는 것 같습니다.

Oscar가 동일한 해시 코드를 가진 동일한 클래스에 대해 말하는 것을 이해하면 주어진 해시 코드를 가진 클래스의 인스턴스 하나만 HashMap에 삽입되도록 제안합니다. 예를 들어 해시 코드가 1 인 SomeClass 인스턴스와 해시 코드가 1 인 SomeClass의 두 번째 인스턴스가있는 경우 SomeClass 인스턴스 하나만 삽입됩니다.

http://pastebin.com/f20af40b9 의 Java pastebin 예제 는 위의 내용이 Oscar가 제안한 내용을 올바르게 요약 한 것으로 보입니다.

에 관계없이 어떤 이해 나 오해, 무슨 일 같은 클래스의 다른 인스턴스가 않는 것입니다 되지 는 키가 동일인지 여부 만 결정되지 때까지 -가 동일한 해시 코드가있는 경우의 HashMap에 한 번만 삽입 얻을. 해시 코드 계약에서는 동일한 객체가 동일한 해시 코드를 가져야합니다. 그러나 동일하지 않은 객체가 다른 해시 코드를 가질 필요는 없습니다 (다른 이유로 바람직 할 수 있음) [1].

pastebin.com/f20af40b9 예제 (Oscar가 적어도 두 번 언급 함)는 다음과 같지만 인쇄 라인이 아닌 JUnit 어설 션을 사용하도록 약간 수정되었습니다. 이 예제는 동일한 해시 코드가 충돌을 일으키고 클래스가 동일 할 때 하나의 항목 만 생성된다는 제안을 지원하는 데 사용됩니다 (예 :이 특정 경우에는 하나의 문자열 만).

@Test
public void shouldOverwriteWhenEqualAndHashcodeSame() {
    String s = new String("ese");
    String ese = new String("ese");
    // same hash right?
    assertEquals(s.hashCode(), ese.hashCode());
    // same class
    assertEquals(s.getClass(), ese.getClass());
    // AND equal
    assertTrue(s.equals(ese));

    Map map = new HashMap();
    map.put(s, 1);
    map.put(ese, 2);
    SomeClass some = new SomeClass();
    // still  same hash right?
    assertEquals(s.hashCode(), ese.hashCode());
    assertEquals(s.hashCode(), some.hashCode());

    map.put(some, 3);
    // what would we get?
    assertEquals(2, map.size());

    assertEquals(2, map.get("ese"));
    assertEquals(3, map.get(some));

    assertTrue(s.equals(ese) && s.equals("ese"));
}

class SomeClass {
    public int hashCode() {
        return 100727;
    }
}

그러나 해시 코드가 완전한 이야기는 아닙니다. pastebin 예제가 무시하는 것은 s및 둘 다 ese동일 하다는 사실입니다. 둘 다 문자열 “ese”입니다. 따라서 s또는 ese또는 "ese"키를 키로 사용하여지도의 콘텐츠를 삽입하거나 가져 오는 것은 모두 동일 s.equals(ese) && s.equals("ese")합니다.

두 번째 테스트는 동일한 클래스의 동일한 해시 코드 가 테스트 1에서 호출 될 때 키-> 값 s -> 1을 덮어 쓰는 이유라는 결론을 내리는 것이 잘못되었음을 보여줍니다 . 시험이에서, 그리고 여전히 같은 해시 코드를 (에 의해 확인으로 ) 그리고 그들은 같은 클래스입니다. 그러나 하고 있는 경우는 없습니다 자바,이 테스트에서 인스턴스 – 유일한 차이점과 같음되는이 테스트에 대한 관련 : 위의 테스트 하나를, 반면에 시험이있는 :ese -> 2map.put(ese, 2)seseassertEquals(s.hashCode(), ese.hashCode());seseMyStringStringString s equals String eseMyStrings s does not equal MyString ese

@Test
public void shouldInsertWhenNotEqualAndHashcodeSame() {
    MyString s = new MyString("ese");
    MyString ese = new MyString("ese");
    // same hash right?
    assertEquals(s.hashCode(), ese.hashCode());
    // same class
    assertEquals(s.getClass(), ese.getClass());
    // BUT not equal
    assertFalse(s.equals(ese));

    Map map = new HashMap();
    map.put(s, 1);
    map.put(ese, 2);
    SomeClass some = new SomeClass();
    // still  same hash right?
    assertEquals(s.hashCode(), ese.hashCode());
    assertEquals(s.hashCode(), some.hashCode());

    map.put(some, 3);
    // what would we get?
    assertEquals(3, map.size());

    assertEquals(1, map.get(s));
    assertEquals(2, map.get(ese));
    assertEquals(3, map.get(some));
}

/**
 * NOTE: equals is not overridden so the default implementation is used
 * which means objects are only equal if they're the same instance, whereas
 * the actual Java String class compares the value of its contents.
 */
class MyString {
    String i;

    MyString(String i) {
        this.i = i;
    }

    @Override
    public int hashCode() {
        return 100727;
    }
}

나중에 언급 한 내용을 바탕으로 오스카는 앞서 말한 내용을 뒤집는 것처럼 보이며 평등의 중요성을 인정합니다. 그러나 “동일한 클래스”가 아니라 동등하다는 개념은 여전히 ​​명확하지 않은 것 같습니다 (내 강조).

“그렇지 않습니다. 목록은 해시가 같지만 키가 다른 경우에만 생성됩니다. 예를 들어 문자열이 해시 코드 2345를 제공하고 정수가 동일한 해시 코드 2345를 제공하면 문자열이 목록에 삽입됩니다. equals (Integer)는 false입니다. 그러나 동일한 클래스 (또는 적어도 .equals가 true를 반환) 가 있으면 동일한 항목이 사용됩니다. 예를 들어 new String ( “one”) 및`new String ( “one”)은 다음과 같이 사용됩니다. 키는 동일한 항목을 사용합니다. 실제로 이것은 처음에 HashMap의 전체 지점입니다. 직접 확인 : pastebin.com/f20af40b9 – Oscar Reyes “

같음에 대한 언급없이 동일한 클래스와 동일한 해시 코드의 중요성을 명시 적으로 설명하는 이전 주석과 비교 :

“@delfuego : 직접보십시오 : pastebin.com/f20af40b9 그래서,이 질문에서 같은 클래스가 사용되고 있습니다 (잠시만, 같은 클래스가 올바르게 사용되고 있습니까?) 이것은 같은 해시가 같은 항목을 사용할 때를 의미합니다 사용되며 항목 “목록”이 없습니다. – Oscar Reyes “

또는

“실제로 이것은 성능을 향상시킬 것입니다. 충돌이 많을수록 해시 테이블 eq의 항목이 적습니다. 할 일이 적습니다. 해시 (잘 보임)도 해시 테이블 (잘 작동 함)도 객체에있을 것입니다. 성능이 저하되는 창작물 – Oscar Reyes “

또는

“@kdgregory : 예,하지만 다른 클래스에서 충돌이 발생하는 경우에만 동일한 클래스 (이 경우)에 대해 동일한 항목이 사용됩니다. – Oscar Reyes”

다시 말하지만, 나는 오스카가 실제로 말하려는 것을 오해 할 수 있습니다. 그러나 그의 원래 의견은 충분한 혼란을 불러 일으켜 일부 명시적인 테스트로 모든 것을 정리하는 것이 현명 해 보이기 때문에 지속적인 의심이 없습니다.


[1] -Joshua Bloch의 Effective Java, Second Edition 에서 :

  • 응용 프로그램을 실행하는 동안 동일한 객체에서 두 번 이상 호출 될 때마다 hashCode 메서드는 객체에 대한 동일한 비교에 사용 된 정보가 수정되지 않는 한 동일한 정수를 일관되게 반환해야합니다. 이 정수는 애플리케이션의 한 실행에서 동일한 애플리케이션의 다른 실행까지 일관성을 유지할 필요가 없습니다.

  • 동일한 s (Obj ect) 메서드에 따라 두 개체가 같은 경우 두 개체 각각에 대해 hashCode 메서드를 호출하면 동일한 정수 결과가 생성되어야합니다.

  • 동일한 s (Object) 메서드에 따라 두 개체가 같지 않은 경우 두 개체 각각에 대해 hashCode 메서드를 호출하면 고유 한 정수 결과가 생성되어야하는 것은 아닙니다. 그러나 프로그래머는 같지 않은 개체에 대해 고유 한 정수 결과를 생성하면 해시 테이블의 성능이 향상 될 수 있음을 알고 있어야합니다.


답변

게시 된 hashCode의 배열이 바이트이면 많은 중복으로 끝날 것입니다.

a [0] + a [1]은 항상 0에서 512 사이입니다. b를 더하면 항상 0에서 768 사이의 숫자가됩니다.이 값을 곱하면 데이터가 완벽하게 분산되어 있다고 가정 할 때 400,000 개의 고유 조합의 상한이됩니다. 각 바이트의 가능한 모든 값 중에서. 데이터가 규칙적인 경우이 방법의 고유 한 출력이 훨씬 적을 수 있습니다.