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構造体によって引き起こされる2つのパフォーマンス問題があります。Console.WriteLine(GC.CollectionCount(0));テストコードに追加したときに表示されるもの。ポイントテストには最大3720コレクションが必要ですが、文字列テストには最大18コレクションしか必要ないことがわかります。無料ではありません。値型が非常に多くのコレクションを誘発する場合、「ええと、あまりにも多くのボクシング」を結論付ける必要があります。

問題は、それHashSet<T>IEqualityComparer<T>その仕事を成し遂げるために必要です。提供していないため、から返されたものにフォールバックする必要がありますEqualityComparer.Default<T>()。そのメソッドは文字列に対して適切に機能し、IEquatableを実装します。ただし、Pointの場合はそうではありません。.NET1.0に似ているタイプであり、ジェネリックに愛されることはありません。Objectメソッドを使用するだけで実行できます。

もう1つの問題は、このテストでは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 2017

32
これは、マウスがウィンドウ内でその位置を通過する方法に触発されました。これは、表示したいビットマップに最適なハッシュです。
Hans Passant 2017

2
それを知ってよかった。あなたのようなハッシュコードを書くためのドキュメントや最高のガイドラインはありますか?実際、上記のハッシュコードがあなたの経験またはあなたが従うガイドラインのどちらに付属しているかを知りたいのです。
Akash KC 2017

5
@AkashKC C#の経験はあまりありませんが、私の知る限り、整数は一般に32ビットです。この場合、2つの数値のハッシュが必要であり、1つの16ビットを左シフトすることにより、各数値の「下位」の16ビットが他の数値に「影響」しないようにし|ます。3つの数値の場合、シフトとして22と11を使用することは理にかなっています。4つの数値の場合、24、16、8になります。ただし、衝突が発生するのは、数値が大きくなった場合のみです。しかし、それはまた、HashSet実装に大きく依存します。「ビットトランケーション」を使用したオープンアドレッシングを使用している場合(そうではないと思います!)、左シフトアプローチは不適切な場合があります。
MSeifert 2017

3
@HansPassant:ポイント座標が16ビットを超える可能性がある場合(おそらく一般的なディスプレイではないが、近い将来)、GetHashCodeでORではなくXORを使用する方が少し良いかもしれません。// XORは通常、ORよりもハッシュ関数の方が優れています。これは、失われる情報が少ないため、リバーシブなどです。//たとえば、負の座標が許可されている場合、Yが負の場合、Xの寄与がどうなるかを検討します。
Krazy Glew 2017

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> (345ミリ秒)よりも高速でした。これはあなたが説明したものとどういうわけか違いますか?
Ahmed Abdelhameed 2017

4
@AhmedAbdelhameedこれはおそらく、ハッシュセットに追加するメンバーが実際よりもはるかに少ないためです(これも、ハッシュコードアルゴリズムの恐ろしい分散が原因です)。データの入力listが完了したら、何回カウントしますか?
InBetween

4
@AhmedAbdelhameedあなたのテストは間違っています。同じlongを繰り返し追加しているので、実際に挿入している要素はほとんどありません。を挿入するpointと、HashSetは内部的に呼び出しGetHashCode、同じハッシュコードを持つそれらの各ポイントに対して、Equalsそれがすでに存在するかどうかを判断するために呼び出します
Ofir Winegarten

49
実装Pointするクラスを作成し、IEqualityComparer<Point>他の機能との互換性を維持しPointながら、貧しい人々がいないという利点を得て、でGetHashCodeボックス化する必要がある場合は、実装する必要はありませんEquals()
ジョンハンナ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.