HashSet <Point>가 HashSet <string>보다 너무 느린 이유는 무엇입니까?


165

중복을 허용하지 않고 일부 픽셀 위치를 저장하고 싶었으므로 가장 먼저 생각해야 할 것은 HashSet<Point>비슷한 클래스입니다. 그러나 이것은 같은 것에 비해 매우 느린 것 같습니다 HashSet<string>.

예를 들어이 코드는 다음과 같습니다.

HashSet<Point> points = new HashSet<Point>();
using (Bitmap img = new Bitmap(1000, 1000))
{
    for (int x = 0; x < img.Width; x++)
    {
        for (int y = 0; y < img.Height; y++)
        {
            points.Add(new Point(x, y));
        }
    }
}

약 22.5 초가 걸립니다.

다음 코드 (명백한 이유로 좋은 선택이 아님) 는 1.6 초 밖에 걸리지 않습니다.

HashSet<string> points = new HashSet<string>();
using (Bitmap img = new Bitmap(1000, 1000))
{
    for (int x = 0; x < img.Width; x++)
    {
        for (int y = 0; y < img.Height; y++)
        {
            points.Add(x + "," + y);
        }
    }
}

그래서 내 질문은 :

  • 그 이유가 있습니까? 이 답변을 확인 했지만 22.5 초는 해당 답변에 표시된 숫자보다 훨씬 큽니다.
  • 중복없이 포인트를 저장하는 더 좋은 방법이 있습니까?


연결된 문자열을 사용하지 않는 "분명한 이유"는 무엇입니까? IEqualityComparer를 구현하고 싶지 않은 경우 더 좋은 방법은 무엇입니까?
Ivan Yurchenko

답변:


290

Point 구조체로 인해 두 가지 성능 문제가 발생합니다. Console.WriteLine(GC.CollectionCount(0));테스트 코드에 추가 할 때 볼 수있는 것 . 포인트 테스트에는 ~ 3720 모음이 필요하지만 문자열 테스트에는 ~ 18 모음 만 필요하다는 것을 알 수 있습니다. 무료가 아닙니다. 값 유형이 너무 많은 컬렉션을 유도하면 "아, 너무 복싱"이라고 결론을 내릴 필요가 있습니다.

문제는 작업을 완료 HashSet<T>해야한다는 IEqualityComparer<T>것입니다. 하나를 제공하지 않았으므로에 의해 반환 된 것으로 대체해야합니다 EqualityComparer.Default<T>(). 이 방법은 문자열에 좋은 일을 할 수 있으며 IEquatable을 구현합니다. 그러나 Point가 아니라 .NET 1.0에서 시작하여 제네릭 사랑을 얻지 못한 유형입니다. Object 메소드 만 사용하면됩니다.

다른 문제는 Point.GetHashCode ()가이 테스트에서 별다른 작업을 수행하지 않으며 너무 많은 충돌이 발생하므로 Object.Equals ()를 상당히 많이 망치는 것입니다. String은 훌륭한 GetHashCode 구현을 가지고 있습니다.

좋은 비교기를 HashSet에 제공하면 두 가지 문제를 모두 해결할 수 있습니다. 이 같은:

class PointComparer : IEqualityComparer<Point> {
    public bool Equals(Point x, Point y) {
        return x.X == y.X && x.Y == y.Y;
    }

    public int GetHashCode(Point obj) {
        // Perfect hash for practical bitmaps, their width/height is never >= 65536
        return (obj.Y << 16) ^ obj.X;
    }
}

그리고 그것을 사용하십시오 :

HashSet<Point> list = new HashSet<Point>(new PointComparer());

이제는 약 150 배 더 빨라져서 쉽게 문자열 테스트를 이길 수 있습니다.


26
GetHashCode 메소드 구현을 제공하는 +1 호기심을 위해서, 당신은 어떻게 특정한 obj.X << 16 | obj.Y;구현 을하게 되었습니까?
Akash KC

32
마우스가 창에서 위치를 전달하는 방식에서 영감을 얻었습니다. 표시하려는 모든 비트 맵에 완벽한 해시입니다.
Hans Passant

2
를 알아서 좋다. 당신과 같은 해시 코드를 작성하는 문서 또는 최상의 지침? 실제로, 나는 여전히 위의 해시 코드가 당신의 경험이나 당신이 따르는 지침과 함께 제공되는지 알고 싶습니다.
Akash KC

5
@ AkashKC C #에 대해서는별로 경험이 없지만 정수는 일반적으로 32 비트라는 것을 알고 있습니다. 이 경우 두 숫자의 해시를 원하고 하나의 16 비트를 왼쪽으로 이동하면 각 숫자의 "하위"16 비트가 다른 숫자에 "영향을 미치지"않도록 |합니다. 3 개의 숫자의 경우 22와 11을 시프트로 사용하는 것이 좋습니다. 4 개의 숫자는 24, 16, 8입니다. 그러나 여전히 충돌이 발생하지만 숫자가 커질 때만 가능합니다. 그러나 그것은 또한 HashSet구현 에 결정적으로 달려 있습니다 . "비트 잘림"으로 열린 주소 지정을 사용하는 경우 (그렇지 않다고 생각합니다!) 왼쪽 이동 방식이 잘못되었을 수 있습니다.
MSeifert

3
@HansPassant : GetHashCode에서 OR 대신 XOR을 사용하는 것이 약간 더 나을지 궁금합니다. 점 좌표가 16 비트를 초과하는 경우 (아마도 일반적인 디스플레이는 아니지만 가까운 미래에). // XOR은 일반적으로 OR보다 해시 함수가 낫습니다. 정보를 잃거나 리버 시브 (reversibke)하는 등입니다. // 예를 들어 음의 좌표가 허용되면 Y가 음이면 X 기여에 어떤 일이 발생하는지 고려하십시오.
Krazy Glew

85

성능 저하의 주된 이유는 모든 권투가 계속되고 있기 때문입니다 ( Hans Passant의 답변 ).

그 외에도 해시 코드 알고리즘은 더 많은 호출을 유발하기 때문에 문제를 악화시킵니다. Equals(object obj) 권투 변환의 양이 증가 .

또한 의 해시 코드는Point 에 의해 계산됩니다 x ^ y. 이로 인해 데이터 범위에서 분산이 거의 HashSet발생하지 않으므로 string해시 분산이 훨씬 큰 버킷 이 과도하게 채워집니다.

자신의 Point구조체 (사소한) 를 구현 하고 예를 들어 좌표를 이동하여 예상 데이터 범위에 대해 더 나은 해시 알고리즘을 사용하여 문제를 해결할 수 있습니다 .

(x << 16) ^ y

해시 코드와 관련하여 좋은 조언을 얻으 려면 주제에 대한 Eric Lippert의 블로그 게시물을 읽으십시오 .


4
포인트의 참조 소스를 보면 GetHashCode: 수행 unchecked(x ^ y)을위한 동안 string이 훨씬 더 복잡 보인다 ..
길 라드 녹색

2
흠 .. 가정이 올바른지 확인하기 위해 방금 HashSet<long>()대신 사용해 보았고 list.Add(unchecked(x ^ y));값을 HashSet에 추가 하는 데 사용 했습니다. 이것은 실제로 HashSet<string> (345ms) 보다 훨씬 빠릅니다 . 이것은 당신이 묘사 한 것과 어떻게 다른가요?
Ahmed Abdelhameed

4
@AhmedAbdelhameed 아마도 해시 코드 알고리즘의 끔찍한 분산으로 인해 알고있는 것보다 적은 수의 멤버를 해시 세트에 추가하기 때문일 것입니다. list당신이 그것을 채우기 완료했을 때 의 카운트는 무엇입니까 ?
InBetween

4
@AhmedAbdelhameed 테스트가 잘못되었습니다. 동일한 long을 반복해서 추가하므로 실제로 삽입하는 요소가 거의 없습니다. 삽입 할 때 point의이 HashSet내부적으로 호출 GetHashCode과 같은 해시 코드와 각각의 포인트에 대한 호출 Equals이 이미 존재하는 것 있는지 확인
피르 Winegarten

49
가난하지 않고 상자에 넣을 필요가 없다는 이점을 얻으면서 작동하는 다른 것들을 Point구현 IEqualityComparer<Point>하고 호환성을 유지 하는 클래스를 만들 수있을 때 구현 할 필요가 Point없습니다 . GetHashCodeEquals()
존 한나
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.