Чому 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?
Іван Юрченко

Відповіді:


290

Є дві перф проблеми, викликані точковою структурою. Щось ви можете побачити, додавши 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
+1 для забезпечення реалізації методу GetHashCode. Тільки для цікавості, як ви прийшли з конкретною obj.X << 16 | obj.Y;реалізацією.
Акаш KC

32
Це надихнуло те, як миша передає своє положення у вікнах. Це ідеальний хеш для будь-яких растрових зображень, які ви хотіли б відображати.
Ганс Пасант

2
Добре це знати. Будь-яка документація чи найкраще керівництво для написання хеш-коду, як ваш? Насправді, я все-таки хотів би знати, чи надходить хеш-код із вашим досвідом чи будь-якими рекомендаціями, яких ви дотримуєтесь.
Акаш KC

5
@AkashKC Я не дуже досвід роботи з C #, але, наскільки я знаю, цілі числа, як правило, 32 біт. У цьому випадку ви хочете хеш з 2 чисел, а зсуваючи один біт на 16 біт, ви переконайтеся, що "нижчі" 16 біт кожного числа не "впливають" на інші |. Для 3-х чисел може мати сенс використовувати 22 і 11 як зсув. Для чотирьох чисел це було б 24, 16, 8. Однак зіткнення все ще будуть, але лише якщо числа будуть великими. Але це також вирішально залежить від HashSetреалізації. Якщо він використовує відкриту адресу з "бітовим укороченням" (я не думаю, що це робить!), Підхід у лівій зміні може бути поганим.
MSeifert

3
@HansPassant: Цікаво, чи використовувати XOR, а не АБО в GetHashCode, може бути трохи краще - у випадку, якщо координати точок можуть перевищувати 16 біт (можливо, не на звичайних дисплеях, але найближчим часом). // XOR, як правило, краще в хеш-функціях, ніж АБО, оскільки він втрачає менше інформації, є реверсивним і т.д. // Наприклад, якщо дозволені негативні координати, подумайте, що відбувається з внеском X, якщо Y негативний.
Krazy Glew

85

Основна причина падіння продуктивності - це все, що відбувається бокс (як уже пояснено у відповіді Ганса Пасанта ).

Крім того, алгоритм хеш-коду погіршує проблему, оскільки викликає більше дзвінків, Equals(object obj)таким чином збільшуючи кількість конверсій боксу.

Також зауважте, що хеш-кодPoint обчислюється x ^ y. Це призводить до дуже малої дисперсії у вашому діапазоні даних, і тому відра HashSetперенаселених - те, що не трапляється string, де дисперсія хешей набагато більша.

Ви можете вирішити цю проблему, застосувавши власну Pointструктуру (тривіальну) та використовуючи кращий хеш-алгоритм для очікуваного діапазону даних, наприклад, зміщуючи координати:

(x << 16) ^ y

Щоб отримати кілька корисних порад щодо хеш-кодів, прочитайте публікацію блогу Еріка Ліпперта .


4
Дивлячись на базове джерело Point, GetHashCodeвиконує: unchecked(x ^ y)тоді як stringце виглядає набагато складніше ..
Gilad Green

2
Хм .. ну, щоб перевірити правильність вашого припущення, я просто спробував використовувати HashSet<long>()замість цього і використовував list.Add(unchecked(x ^ y));для додавання значень до HashSet. Це було насправді навіть швидше, ніж HashSet<string> (345 мс) . Це чимось відрізняється від описаного вами?
Ахмед Абдельхамед

4
@AhmedAbdelhameed, мабуть, тому, що ви додаєте в свій хеш-набір значно менше членів, ніж ви усвідомлюєте (знову ж таки через жахливу дисперсію алгоритму хеш-коду). Яка кількість рахунків, listколи ви закінчите її заселяти?
Між

4
@AhmedAbdelhameed Ваш тест невірний. Ви додаєте однакові довгі і знову, так що насправді є лише кілька елементів, які ви вставляєте. При вставці point, то HashSetбуде внутрішньо зателефонувати GetHashCodeі для кожної з цих точок з одного і того ж хеш - код, подзвонить , Equalsщоб визначити , якщо він вже існує
Офір Winegarten

49
Не потрібно реалізовувати, Pointколи ви можете створити клас, який реалізовує IEqualityComparer<Point>та зберігає сумісність з іншими речами, з якими працює Point, отримуючи при цьому користь від того, що бідні не GetHashCodeпотребують, і необхідність приймати участь у них Equals().
Джон Ханна
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.