Java HashMap 성능 최적화 / 대안


102

큰 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만으로 설정 하면 성능이 저하 됩니다.

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


29
HashMap이 느려지면 해시 함수가 충분하지 않습니다.
Pascal Cuoq

12
의사, 내가 이것을
skaffman

12
이것은 정말 좋은 질문입니다. 해싱 알고리즘이 중요한 이유와 성능에 미치는 영향에 대한 멋진 데모
oxbow_lakes

12
a의 합은 0 ~ 102의 범위를 가지며 b의 합은 0 ~ 153의 범위를 가지므로 가능한 해시 값은 15,606 개 뿐이고 hashCode가 동일한 키의 평균은 1,666 개입니다. 가능한 hashCode의 수가 키의 수보다 훨씬 많도록 해시 코드를 변경해야합니다.
Peter Lawrey 2009

6
나는 당신이 Texas Hold 'Em Poker를 모델링하고 있다고 심령 적으로 결정했습니다 ;-)
bacar

답변:


56

많은 사람들이 지적했듯이 그 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

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


17
hashCode메서드 에서 배열을 수정하지 않는 것이 좋습니다 . 관례 상, hashCode객체의 상태를 변경하지 않습니다. 아마도 생성자는 그들을 정렬하기에 더 좋은 곳일 것입니다.
Michael Myers

생성자에서 배열 정렬이 이루어져야한다는 데 동의합니다. 표시된 코드가 hashCode를 설정하지 않는 것 같습니다. 코드 계산은 다음과 같이 더 간단하게 수행 할 수 있습니다 int result = a[0]; result = result * 52 + a[1]; //etc..
rsp

생성자에서 정렬 한 다음 mmyers 및 rsp가 제안하는대로 해시 코드를 계산하는 것이 더 낫다는 데 동의합니다. 제 경우에는 내 솔루션이 허용 가능하며 배열 hashCode()이 작동 하려면 정렬되어야한다는 사실을 강조하고 싶었습니다 .
nash

3
해시 코드를 캐시 할 수도 있습니다 (그리고 객체가 변경 가능한 경우 적절하게 무효화).
궁둥이

1
그냥 사용 java.util.Arrays.hashCode을 () . 더 간단하고 (직접 작성하고 유지 관리 할 코드가 없음) 계산이 더 빠르며 (더 적은 곱셈) 해시 코드의 분포가 더 균일 할 것입니다.
jcsahnwaldt Monica 복원

18

나는 당신에 통지 한 가지 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필요한 경우). 그것은 일을 조금 더 균등하게해야합니다.


나는 또한 다른 순서로 동일한 요소를 가진 배열이 동일한 해시 코드를 제공하는 속성을 원하기 때문에 나를 위해 작동하지 않을 것이라고 추가해야하지만.
nash 2011

5
이 경우 52C2 + 52C3 해시 코드 (제 계산기에 따르면 23426)가 있으며 해시 맵은 작업에 매우 잘못된 도구입니다.
kdgregory

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

7
@Oscar- 이제 해시 체인의 선형 검색을 수행해야하므로 충돌이 많을수록 더 많은 작업을 수행 할 수 있습니다. equals () 당 고유 값이 26,000,000 개이고 hashCode () 당 고유 값이 26,000 개인 경우 버킷 체인에는 각각 1,000 개의 객체가 있습니다.
kdgregory

@ Nash0 : 당신은 이것들이 동일한 hashCode를 갖기를 원하지만 동시에 같지 않기를 원한다고 말하는 것 같습니다 (equals () 메소드에 의해 정의 된대로). 왜 그걸 원 하겠어요?
MAK

17

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 주기만 소모됩니다. 여기서 교훈 : 해시 함수에 복잡성을 추가하는 것은 목표가 아닙니다. 목표는 복잡성을 위해 복잡성을 추가하는 것이 아니라 다양한 가치를 얻는 것입니다.


3
네, 잘못된 해시 함수는 이런 종류의 동작을 초래합니다. +1
Henning

별로. 목록은 해시가 동일하지만 키가 다른 경우 에만 생성 됩니다 . 때문에 문자열주고 해시 코드 2345와 및 정수가 같은 해시 코드 2345을 제공하는 경우 예를 들어, 다음 정수 목록에 삽입 된다 . 그러나 동일한 클래스가 있거나 적어도 true를 반환하면 동일한 항목이 사용됩니다. 예를 들어 키로 사용되는`new String ( "one")은 동일한 항목을 사용합니다. 사실 이것은 처음에 HashMap 의 전체 포인트입니다! 직접 확인 : pastebin.com/f20af40b9String.equals( Integer )false.equalsnew String("one")
OscarRyz

3
@Oscar : 내 원래 게시물에 추가 된 내 답글을 참조하십시오.
Jay

나는 이것이 매우 오래된 스레드라는 것을 알고 있지만 여기에 해시 코드와 관련된 "충돌"이라는 용어에 대한 참조가 있습니다 : link . 당신이 동일한 키와 다른 값을 넣어 해시 맵에 값을 교체 할 때, 그것은되어 있지 충돌라고
타히르 악 타르

@Tahir 정확합니다. 아마도 내 게시물이 잘못 쓰여진 것 같습니다. 설명해 주셔서 감사합니다.
Jay

7

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

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

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


나는 그들이 다시 계산되었다고 생각하지 않습니다. 테이블 크기가 증가하고 해시가 유지됩니다.
Henning

Hashmap은 모든 항목에 대해 비트 단위로 수행합니다. newIndex = storedHash & newLength;
Henning

4
Hanning : 아마도 delfuego의 말이 좋지 않을 수도 있지만 요점은 유효합니다. 예, hashCode ()의 출력이 다시 계산되지 않는다는 의미에서 해시 값이 다시 계산되지 않습니다. 그러나 테이블 크기가 증가하면 모든 키를 테이블에 다시 삽입해야합니다. 즉, 테이블에서 새 슬롯 번호를 얻으려면 해시 값을 다시 해시해야합니다.
Jay

제이, 맞아요-정말 형편없는 말투와 당신이 말한 것. :)
delfuego

1
@delfuego 및 @ nash0 : 맞습니다. 초기 용량을 요소 수와 동일하게 설정하면 수백만 번의 충돌이 발생하여 해당 용량의 소량 만 사용하기 때문에 성능이 저하됩니다. 사용 가능한 모든 항목을 사용 하더라도 동일한 용량을 설정하면 최악의 상황이됩니다.로드 팩터로 인해 더 많은 공간이 요청되기 때문입니다. initialcapactity = maxentries/loadcapacity(예 : 26M 항목에 대해 30M, 0.95) 를 사용해야 하지만 , 약 20k 이하 만 사용하는 모든 충돌이 발생하므로 이것은 귀하의 경우 가 아닙니다 .
OscarRyz

7

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

  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 메서드보다 충돌 가능성이 훨씬 적다고 확신합니다.


이러한 종류의 맵과 배열에서는 RAM이 작을 수 있으므로 이미 메모리 제한 문제를 의심했습니다.
ReneS

7

"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 메서드를 호출하면 고유 한 정수 결과가 생성되어야하는 것은 아닙니다. 그러나 프로그래머는 같지 않은 개체에 대해 고유 한 정수 결과를 생성하면 해시 테이블의 성능이 향상 될 수 있음을 알고 있어야합니다.


5

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

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


4

HashMap은 초기 용량을 가지고 있으며 HashMap의 성능은 기본 객체를 생성하는 hashCode에 매우 크게 의존합니다.

둘 다 조정하십시오.


4

키에 패턴이있는 경우 맵을 더 작은 맵으로 분할하고 인덱스 맵을 가질 수 있습니다.

예 : 키 : 1,2,3, .... n 각각 100 만 개의지도 28 개. 인덱스 맵 : 1-1,000,000-> Map1 1,000,000-2,000,000-> Map2

따라서 두 번의 조회를 수행하지만 키 세트는 1,000,000 대 28,000,000이됩니다. 스팅 패턴으로도 쉽게 할 수 있습니다.

키가 완전히 무작위이면 작동하지 않습니다.


1
키가 무작위 인 경우에도 (key.hashCode () % 28)을 사용하여 해당 키-값을 저장할 맵을 선택할 수 있습니다.
Juha Syrjälä

4

언급 한 2 바이트 배열이 전체 키이고 값이 0-51 범위에 있고 고유하며 a 및 b 배열 내의 순서가 중요하지 않은 경우 내 수학에 따르면 가능한 순열은 약 2,600 만 개 뿐이며 가능한 모든 키의 값으로 맵을 채우려 고 할 것입니다.

이 경우 HashMap 대신 배열을 사용하고 0에서 25989599까지 인덱싱하면 데이터 저장소에서 값을 채우고 검색하는 것이 훨씬 더 빠릅니다.


그것은 매우 좋은 생각이며, 사실 저는 12 억 개의 요소가있는 또 다른 데이터 저장 문제에 대해 그렇게하고 있습니다. 이 경우에는 쉬운 방법을 선택하고 미리 만들어진 데이터 구조를 사용하고 싶었습니다. :)
nash

4

여기 늦었지만 큰지도에 대한 몇 가지 의견이 있습니다.

  1. 다른 게시물에서 자세히 논의했듯이 좋은 hashCode ()를 사용하면 Map의 26M 항목은 큰 문제가 아닙니다.
  2. 그러나 여기서 잠재적으로 숨겨진 문제는 거대한지도의 GC 영향입니다.

나는이지도들이 오래 살았다 고 가정하고 있습니다. 즉, 당신이 그들을 채우고 그들은 앱이 지속되는 동안 주위에 붙어 있습니다. 나는 또한 앱 자체가 일종의 서버처럼 오래 살았다 고 가정합니다.

Java HashMap의 각 항목에는 키, 값 및 이들을 함께 묶는 항목이라는 세 개의 오브젝트가 필요합니다. 따라서 맵의 26M 항목은 26M * 3 == 78M 개체를 의미합니다. 전체 GC에 도달 할 때까지 괜찮습니다. 그렇다면 세계 일시 중지 문제가 있습니다. GC는 78M 개체를 각각 살펴보고 모두 살아 있는지 확인합니다. 7,800 만 개 이상의 개체는 볼 수있는 개체가 많습니다. 앱이 가끔 긴 (아마도 몇 초) 일시 중지를 허용 할 수 있다면 문제가 없습니다. 지연 시간 보장을 달성하려는 경우 중요한 문제가있을 수 있습니다 (물론 지연 시간 보장을 원하는 경우 Java는 선택할 플랫폼이 아닙니다. :)) 맵의 값이 빠르게 변동되면 빈번한 전체 수집으로 끝날 수 있습니다. 문제를 크게 복잡하게 만듭니다.

이 문제에 대한 훌륭한 해결책을 모르겠습니다. 아이디어 :

  • 때로는 전체 GC를 "대부분"방지하도록 GC 및 힙 크기를 조정할 수 있습니다.
  • 맵 콘텐츠가 많이 흔들리는 경우 Javolution의 FastMap을 사용해 볼 수 있습니다. 엔트리 개체를 풀링하여 전체 수집 빈도를 낮출 수 있습니다.
  • 자신 만의 맵 impl을 생성하고 byte []에서 명시적인 메모리 관리를 수행 할 수 있습니다 (예 : 수백만 개의 객체를 단일 바이트 []로 직렬화하여 더 예측 가능한 대기 시간을 위해 CPU를 교환합니다. 앗!).
  • 이 부분에 Java를 사용하지 마십시오. 소켓을 통해 일종의 예측 가능한 메모리 내 DB와 대화하십시오.
  • 새로운 G1 수집가가 도움이 되기를 바랍니다 (주로 이탈률이 높은 케이스에 적용됨).

자바에서 거대한지도를 가지고 많은 시간을 보낸 사람의 생각입니다.



3

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

내 실험에서 (2009 년 학생 프로젝트) :

  • 1에서 100.000까지 100.000 노드에 대해 Red Black Tree를 구축했습니다. 785.68 초 (13 분)가 걸렸습니다. 그리고 1 백만 노드에 대한 RBTree를 구축하지 못했습니다 (HashMap을 사용한 결과처럼).
  • 내 알고리즘 데이터 구조 인 "Prime Tree"를 사용합니다. 21.29 초 (RAM : 1.97Gb) 내에 1,000 만 개의 노드에 대한 트리 / 맵을 구축 할 수 있습니다. 검색 키-값 비용은 O (1)입니다.

참고 : "Prime Tree"는 1-1,000 만 사이의 "연속 키"에서 가장 잘 작동합니다. HashMap과 같은 키로 작업하려면 약간의 조정이 필요합니다.


그렇다면 #PrimeTree는 무엇입니까? 간단히 말해, 이진 트리와 같은 트리 데이터 구조이며 분기 번호는 "2"-이진 대신 소수입니다.


링크 나 구현을 공유해 주시겠습니까?
Benj

2

HSQLDB 와 같은 메모리 내 데이터베이스를 사용해 볼 수 있습니다 .



1

이를 위해 내장 된 데이터베이스 사용을 고려해 보셨습니까? 봐 버클리 DB . 현재 Oracle 소유의 오픈 소스입니다.

모든 것을 Key-> Value 쌍으로 저장하며 RDBMS가 아닙니다. 빠른 것을 목표로합니다.


2
Berkeley DB는 직렬화 / IO 오버 헤드로 인해이 항목 수에 비해 빠르지 않습니다. 해시 맵보다 빠를 수 없으며 OP는 지속성을 신경 쓰지 않습니다. 귀하의 제안은 좋은 것이 아닙니다.
oxbow_lakes

1

먼저 다른 많은 답변이 설명하는 것처럼 Map을 올바르게 사용하고 있는지, 키에 대한 좋은 hashCode () 메서드, Map의 초기 용량, 올바른 Map 구현 등을 확인해야합니다.

그런 다음 프로파일 러를 사용하여 실제로 일어나는 일과 실행 시간이 어디에 있는지 확인하는 것이 좋습니다. 예를 들어 hashCode () 메서드가 수십억 번 실행됩니까?

그래도 도움이되지 않는다면 EHCache 또는 memcached 같은 것을 사용하는 것은 어떻습니까? 예, 캐싱 용 제품이지만 충분한 용량을 갖고 캐시 스토리지에서 값을 제거하지 않도록 구성 할 수 있습니다.

또 다른 옵션은 전체 SQL RDBMS보다 가벼운 데이터베이스 엔진입니다. 아마도 Berkeley DB 와 같은 것입니다.

개인적으로 이러한 제품의 성능에 대한 경험은 없지만 시도해 볼 가치가 있습니다.


1

계산 된 해시 코드를 키 개체에 캐시 할 수 있습니다.

이 같은:

public int hashCode() {
  if(this.hashCode == null) {
     this.hashCode = computeHashCode();
  }
  return this.hashCode;
}

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

물론 처음으로 hashCode를 계산 한 후에는 키의 내용을 변경하지 않도록주의해야합니다.

편집 : 맵에 각 키를 한 번만 추가 할 때 캐시에 코드 값이있는 것은 가치가없는 것 같습니다. 다른 상황에서는 유용 할 수 있습니다.


아래에서 지적한 바와 같이, 크기를 조정할 때 HashMap에서 객체의 해시 코드를 재 계산하지 않으므로 아무것도 얻지 못합니다.
delfuego

1

다른 포스터는 이미 값을 함께 추가하는 방식으로 인해 해시 코드 구현으로 인해 많은 충돌이 발생할 것이라고 지적했습니다. 디버거에서 HashMap 객체를 보면 매우 긴 버킷 체인과 함께 200 개의 고유 한 해시 값이 있다는 것을 알 수 있습니다.

항상 0..51 범위의 값이있는 경우 각 값은 6 비트를 사용하여 나타냅니다. 항상 5 개의 값이있는 경우 왼쪽 시프트 및 추가를 사용하여 30 비트 해시 코드를 만들 수 있습니다.

    int code = a[0];
    code = (code << 6) + a[1];
    code = (code << 6) + b[0];
    code = (code << 6) + b[1];
    code = (code << 6) + b[2];
    return code;

왼쪽 시프트는 빠르지 만 균등하게 분산되지 않은 해시 코드를 남깁니다 (6 비트는 0..63 범위를 의미하기 때문). 대안은 해시에 51을 곱하고 각 값을 더하는 것입니다. 이것은 여전히 ​​완벽하게 분산되지 않으며 (예 : {2,0} 및 {1,52} 충돌) 시프트보다 느립니다.

    int code = a[0];
    code *= 51 + a[1];
    code *= 51 + b[0];
    code *= 51 + b[1];
    code *= 51 + b[2];
    return code;

@kdgregory : "더 많은 충돌은 다른 곳에서 더 많은 작업을 의미합니다."에 대해 대답했습니다. :)
OscarRyz

1

지적했듯이 해시 코드 구현에는 충돌이 너무 많으며이를 수정하면 적절한 성능을 얻을 수 있습니다. 또한 hashCode를 캐싱하고 동등성을 효율적으로 구현하면 도움이 될 것입니다.

더욱 최적화해야하는 경우 :

설명에 따르면 (52 * 51 / 2) * (52 * 51 * 50/6) = 29304600 개의 다른 키만 있습니다 (이 중 26000000, 즉 약 90 %가 표시됨). 따라서 충돌없이 해시 함수를 설계하고 해시 맵 대신 간단한 배열을 사용하여 데이터를 보관하여 메모리 소비를 줄이고 조회 속도를 높일 수 있습니다.

T[] array = new T[Key.maxHashCode];

void put(Key k, T value) {
    array[k.hashCode()] = value;

T get(Key k) {
    return array[k.hashCode()];
}

(일반적으로 잘 클러스터링되는 효율적이고 충돌없는 해시 함수를 설계하는 것은 불가능합니다. 이것이 HashMap이 충돌을 허용하여 약간의 오버 헤드가 발생하는 이유입니다)

가정 ab분류되어하는 것은 다음과 같은 해시 함수를 사용할 수 있습니다 :

public int hashCode() {
    assert a[0] < a[1]; 
    int ahash = a[1] * a[1] / 2 
              + a[0];

    assert b[0] < b[1] && b[1] < b[2];

    int bhash = b[2] * b[2] * b[2] / 6
              + b[1] * b[1] / 2
              + b[0];
    return bhash * 52 * 52 / 2 + ahash;
}

static final int maxHashCode = 52 * 52 / 2 * 52 * 52 * 52 / 6;  

나는 이것이 충돌이 없다고 생각합니다. 이것을 증명하는 것은 수학적인 경향이있는 독자를위한 연습으로 남겨집니다.


1

에서 효과적인 자바 : 프로그래밍 언어 가이드 (자바 시리즈)

3 장에서는 hashCode ()를 계산할 때 따라야 할 좋은 규칙을 찾을 수 있습니다.

특별히:

필드가 배열이면 각 요소가 별도의 필드 인 것처럼 처리합니다. 즉, 이러한 규칙을 재귀 적으로 적용하여 각 중요한 요소에 대한 해시 코드를 계산하고 단계 2.b마다 이러한 값을 결합합니다. 배열 필드의 모든 요소가 중요한 경우 릴리스 1.5에 추가 된 Arrays.hashCode 메서드 중 하나를 사용할 수 있습니다.


0

처음에는 큰지도를 할당하십시오. 2,600 만 개의 항목이 있고 이에 대한 메모리가있는 경우 new HashMap(30000000).

2,600 만 개의 키와 값이있는 2,600 만 개의 항목을위한 충분한 메모리가 있습니까? 이것은 나에게 많은 기억처럼 들립니다. 가비지 컬렉션이 2 ~ 3 백만 마크에서 여전히 잘 작동하고 있습니까? 병목 현상으로 상상할 수 있습니다.


2
아, 또 하나. 해시 코드는 맵의 단일 위치에서 큰 연결 목록을 피하기 위해 균등하게 배포되어야합니다.
ReneS

0

다음 두 가지를 시도해 볼 수 있습니다.

  • 귀하의 확인 hashCode등의 연속 int로서 방법 반환 뭔가 간단하고 효과적

  • 지도를 다음과 같이 초기화합니다.

    Map map = new HashMap( 30000000, .95f );

이 두 가지 조치는 구조가 수행하는 리 해싱의 양을 엄청나게 줄일 것이며 제 생각에 테스트하기 매우 쉽습니다.

그래도 작동하지 않으면 RDBMS와 같은 다른 저장소를 사용하는 것이 좋습니다.

편집하다

초기 용량을 설정하면 귀하의 경우 성능이 저하되는 것이 이상합니다.

javadocs 에서 참조하십시오 .

초기 용량이 최대 항목 수를로드 계수로 나눈 값보다 크면 재해시 작업이 발생하지 않습니다.

나는 microbeachmark를 만들었습니다 (어떤 수단으로도 결정적이지는 않지만 적어도이 점을 증명합니다)

$cat Huge*java
import java.util.*;
public class Huge {
    public static void main( String [] args ) {
        Map map = new HashMap( 30000000 , 0.95f );
        for( int i = 0 ; i < 26000000 ; i ++ ) { 
            map.put( i, i );
        }
    }
}
import java.util.*;
public class Huge2 {
    public static void main( String [] args ) {
        Map map = new HashMap();
        for( int i = 0 ; i < 26000000 ; i ++ ) { 
            map.put( i, i );
        }
    }
}
$time java -Xms2g -Xmx2g Huge

real    0m16.207s
user    0m14.761s
sys 0m1.377s
$time java -Xms2g -Xmx2g Huge2

real    0m21.781s
user    0m20.045s
sys 0m1.656s
$

따라서 초기 용량을 사용하면 재조정으로 인해 21 초에서 16 초로 감소합니다. 그것은 우리에게 당신의 hashCode방법을 "기회의 영역"으로 남겨둔다 ;)

편집하다

HashMap이 아닙니다.

마지막 버전에 따라.

응용 프로그램을 실제로 프로파일 링하고 메모리 / cpu가 소비되는 위치를 확인해야한다고 생각합니다.

나는 당신을 구현하는 클래스를 만들었습니다. hashCode

이 해시 코드는 수백만 번의 충돌을 제공하며 HashMap의 항목은 극적으로 감소합니다.

이전 테스트의 21, 16 초에서 10 초와 8 초로 통과했습니다. 그 이유는 hashCode가 많은 수의 충돌을 유발하고 당신이 생각하는 26M 객체를 저장하지 않고 훨씬 더 적은 수 (약 20k라고 말할 것입니다)를 저장하기 때문입니다.

문제 는 HASHMAP 이 코드의 다른 곳에 있다는 것입니다.

이제 프로파일 러를 구하고 어디를 찾아야 할 때입니다. 나는 그것이 항목의 생성에 있거나 아마도 디스크에 쓰거나 네트워크에서 데이터를 받고 있다고 생각합니다.

여기에 당신의 수업을 구현했습니다.

참고 로 0-51 범위를 사용하지 않았지만 내 값에 -126에서 127까지 반복을 인정합니다. 질문을 업데이트하기 전에이 테스트를 수행했기 때문입니다.

유일한 차이점은 클래스가 더 많은 충돌을 가지므로 맵에 저장된 항목이 적다는 것입니다.

import java.util.*;
public class Item {

    private static byte w = Byte.MIN_VALUE;
    private static byte x = Byte.MIN_VALUE;
    private static byte y = Byte.MIN_VALUE;
    private static byte z = Byte.MIN_VALUE;

    // Just to avoid typing :) 
    private static final byte M = Byte.MAX_VALUE;
    private static final byte m = Byte.MIN_VALUE;


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

    public Item () {
        // make a different value for the bytes
        increment();
        a[0] = z;        a[1] = y;    
        b[0] = x;        b[1] = w;   b[2] = z;
    }

    private static void increment() {
        z++;
        if( z == M ) {
            z = m;
            y++;
        }
        if( y == M ) {
            y = m;
            x++;
        }
        if( x == M ) {
            x = m;
            w++;
        }
    }
    public String toString() {
        return "" + this.hashCode();
    }



    public int hashCode() {
        int hash = 503;
        hash = hash * 5381 + (a[0] + a[1]);
        hash = hash * 5381 + (b[0] + b[1] + b[2]);
        return hash;
    }
    // I don't realy care about this right now. 
    public boolean equals( Object other ) {
        return this.hashCode() == other.hashCode();
    }

    // print how many collisions do we have in 26M items.
    public static void main( String [] args ) {
        Set set = new HashSet();
        int collisions = 0;
        for ( int i = 0 ; i < 26000000 ; i++ ) {
            if( ! set.add( new Item() ) ) {
                collisions++;
            }
        }
        System.out.println( collisions );
    }
}

이 클래스를 사용하면 이전 프로그램의 키가 있습니다.

 map.put( new Item() , i );

나에게 준다 :

real     0m11.188s
user     0m10.784s
sys 0m0.261s


real     0m9.348s
user     0m9.071s
sys  0m0.161s

3
(귀하의 의견에 대한 응답으로) 위의 다른 곳에서 지적했듯이 Oscar는 더 많은 충돌이 좋은 것이라고 가정하는 것 같습니다. 그것은 매우 좋지 않습니다. 충돌은 주어진 해시의 슬롯이 단일 항목 포함에서 항목 목록 포함으로 이동하고 슬롯에 액세스 할 때마다이 목록을 검색 / 순회해야 함을 의미합니다.
delfuego

@delfuego : 그렇지 않습니다. 다른 클래스를 사용하는 충돌이있을 때만 발생하지만 동일한 클래스에 대해 동일한 항목이 사용됩니다.)
OscarRyz

2
@Oscar-MAK의 답변으로 귀하에 대한 내 답변을 참조하십시오. HashMap은 각 해시 버킷에 연결된 항목 목록을 유지하고 모든 요소에서 equals ()를 호출하여 해당 목록을 탐색합니다. 객체의 클래스는 그것과 관련이 없습니다 (equals ()에 대한 단락을 제외하고).
kdgregory

1
@Oscar-답변을 읽으면 해시 코드가 동일하면 equals ()가 true를 반환한다고 가정하는 것 같습니다. 이것은 equals / hashcode 계약의 일부가 아닙니다. 내가 잘못 이해했다면이 댓글을 무시하세요.
kdgregory

1
Oscar의 노력에 대해 대단히 감사하지만, 동일한 해시 코드를 갖는 것과 동일한 키 객체를 혼동하고 있다고 생각합니다. 또한 동일한 문자열을 키로 사용하는 코드 링크 중 하나에서 Java의 문자열은 변경할 수 없음을 기억하십시오. 나는 우리 둘 다 오늘 해싱에 대해 많은 것을 배웠다고 생각한다 :)
nash


0

나는 목록 대 해시 맵으로 잠시 전에 작은 테스트를 수행했습니다. 재미있는 것은 목록을 반복하고 해시 맵 get 기능을 사용하는 것과 동일한 시간이 밀리 초 단위로 걸린다는 것입니다. 그 크기의 해시 맵으로 작업 할 때 메모리는 큰 문제입니다.


당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.