web-dev-qa-db-ja.com

HashSet <Point>がHashSet <string>よりもずっと遅いのはなぜですか?

重複を許可せずにいくつかのピクセルの場所を保存したかったので、最初に思い浮かぶのは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秒はその回答に示されている数値よりもはるかに長いです。
  • 重複せずにポイントを保存するより良い方法はありますか?
161

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

問題は、HashSet<T>IEqualityComparer<T>を必要とすることです。提供しなかったため、EqualityComparer.Default<T>()によって返されたものにフォールバックする必要があります。このメソッドは文字列に対して良い仕事をすることができ、IEquatableを実装します。しかし、Pointの場合ではなく、.NET 1.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倍高速になり、文字列テストを簡単に破ります。

283
Hans Passant

パフォーマンスの低下の主な理由は、進行中のすべてのボクシングです(すでに Hans Passant's answerで説明されているように)。

それとは別に、ハッシュコードアルゴリズムは、Equals(object obj)の呼び出しを増やすため、問題を悪化させます。したがって、ボクシング変換の量が増加します。

Pointのハッシュコードx ^ yによって計算されることにも注意してください。これにより、データ範囲の分散が非常に小さくなるため、HashSetのバケットが過密になります。これは、stringでは発生しない、ハッシュの分散がはるかに大きいものです。

独自のPoint構造体(簡単な)を実装し、予想されるデータ範囲に対してより良いハッシュアルゴリズムを使用することで、この問題を解決できます。座標をシフトすることにより:

(x << 16) ^ y

ハッシュコードに関する良いアドバイスについては、 この件に関するEric Lippertのブログ投稿 をお読みください。

86
InBetween