it-swarm.com.de

Warum ist HashSet <Point> so viel langsamer als HashSet <string>?

Ich wollte einige Pixelpositionen speichern, ohne Duplikate zuzulassen, daher fallen mir zuerst HashSet<Point> Oder ähnliche Klassen ein. Dies scheint jedoch im Vergleich zu etwas wie HashSet<string> Sehr langsam zu sein.

Zum Beispiel dieser Code:

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));
        }
    }
}

dauert ca. 22,5 Sekunden.

Während der folgende Code (was aus offensichtlichen Gründen keine gute Wahl ist) nur 1,6 Sekunden dauert:

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);
        }
    }
}

Meine Fragen lauten also:

  • Gibt es einen Grund dafür? Ich habe diese Antwort angekreuzt, aber 22,5 Sekunden sind weit mehr als die in dieser Antwort gezeigten Zahlen.
  • Gibt es eine bessere Möglichkeit, Punkte ohne Duplikate zu speichern?
164

Es gibt zwei durch die Point-Struktur hervorgerufene Leistungsprobleme. Das können Sie sehen, wenn Sie Console.WriteLine(GC.CollectionCount(0)); zum Testcode hinzufügen. Sie werden sehen, dass der Point-Test ~ 3720 Sammlungen benötigt, der String-Test jedoch nur ~ 18 Sammlungen. Nicht gratis. Wenn Sie sehen, dass ein Werttyp so viele Sammlungen induziert, müssen Sie "äh, zu viel Boxen" schließen.

Es geht darum, dass HashSet<T> Einen IEqualityComparer<T> Benötigt, um seine Arbeit zu erledigen. Da Sie keine angegeben haben, muss auf eine zurückgegriffen werden, die von EqualityComparer.Default<T>() zurückgegeben wurde. Diese Methode kann gute Arbeit für Zeichenfolgen leisten, sie implementiert IEquatable. Aber nicht für Point, es ist ein Typ, der von .NET 1.0 abstammt und nie die allgemeine Liebe bekam. Alles was es tun kann, ist die Object-Methoden zu verwenden.

Das andere Problem ist, dass Point.GetHashCode () in diesem Test keine herausragende Arbeit leistet, da zu viele Kollisionen auftreten und Object.Equals () daher ziemlich stark gehämmert wird. String hat eine ausgezeichnete GetHashCode-Implementierung.

Sie können beide Probleme lösen, indem Sie dem HashSet einen guten Vergleicher zur Verfügung stellen. Wie dieser:

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;
    }
}

Und benutze es:

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

Und es ist jetzt etwa 150-mal schneller und schlägt den Saitentest problemlos.

285
Hans Passant

Der Hauptgrund für den Leistungsabfall ist das Boxen (wie bereits in Hans Passants Antwort erklärt).

Abgesehen davon verschlimmert der Hash-Code-Algorithmus das Problem, da er mehr Aufrufe von Equals(object obj) verursacht und somit die Anzahl der Boxing-Conversions erhöht.

Beachten Sie auch, dass der Hash-Code von Point von x ^ y Berechnet wird. Dies führt zu einer sehr geringen Streuung in Ihrem Datenbereich, und daher sind die Buckets von HashSet überfüllt - etwas, was bei string nicht der Fall ist, bei dem die Streuung der Hashes viel größer ist.

Sie können dieses Problem lösen, indem Sie Ihre eigene Point -Struktur (trivial) implementieren und einen besseren Hash-Algorithmus für Ihren erwarteten Datenbereich verwenden, z. durch Verschieben der Koordinaten:

(x << 16) ^ y

Lesen Sie Eric Lipperts Blog-Post zu diesem Thema , um einige gute Ratschläge zu Hash-Codes zu erhalten.

86
InBetween