Предоставление хорошей хеш-функции

В моем первоначальном вопросе (подробное экспериментальное исследование): -ve">Подходящий контейнер для быстрой вставки и поиска n-мерных действительных векторов (предоставлен начальный бенчмаркинг) Я получил действительно странное поведение, используя несортированный набор для управления случайными N-мерными массивами с плавающей запятой с моим начальным (вероятно плохо спроектированная хэш-функция):

#include <iostream>
#include <chrono>
#include <random>
#include <array>
#include <unordered_set>

const int N = 3;  // Dimensionality of the arrays

std::array<double, N> getRandomArray() {
  // Engines and distributions retain state, thus defined as static
  static std::default_random_engine e;                    // engine
  static std::uniform_real_distribution<double> d(0, 1);  // distribution
  std::array<double, N> ret;
  for (size_t i = 0; i < N; ++i) {
    ret[i] = d(e);
  }
  return ret;
}

// Return Squared Euclidean Distance
template <typename InputIt1, typename InputIt2>
double EuclideanDistance2(InputIt1 beg1, InputIt1 end1, InputIt2 beg2) {
  double val = 0.0;
  while (beg1 != end1) {
    double dist = (*beg1++) - (*beg2++);
    val += dist*dist;
  }
  return val;
}

struct ArrayHash {  // Hash Function
  std::size_t operator() (const std::array<double, N>& arr) const {
    std::size_t ret = 0;
    for (const double elem : arr) {
      ret += std::hash<double>()(elem);
    }
    return ret;
  }
};

struct ArrayEqual {  // Equivalence Criterion
  bool operator() (const std::array<double, N>& arr1,
                          const std::array<double, N>& arr2) const {
    return EuclideanDistance2(arr1.begin(), arr1.end(), arr2.begin()) < tol*tol;
  }
 private:
  static constexpr double tol = 1e-6;  // Comparison tolerance
};


int main() {
  // create a unordered set of double arrays (usda)
  std::unordered_set<std::array<double, N>, ArrayHash, ArrayEqual> usda;
  // record start time
  auto start = std::chrono::steady_clock::now();
  // Generate and insert one hundred thousands new double arrays
  for (size_t i = 0; i < 100000; ++i) {
    // Get a new random double array (da)
    std::array<double, N> da = getRandomArray();
    usda.insert(da);
  }
  // record finish time
  auto end = std::chrono::steady_clock::now();
  std::chrono::duration<double> diff = end - start;
  std::cout << "Time to generate and insert unique elements into UNORD. SET: "
            << diff.count() << " s\n";
  std::cout << "unord. set size() = " << usda.size() << std::endl;
  return 0;
}

Две самые странные вещи:

  1. При проведении экспериментов без каких-либо флагов оптимизации даже с нестрогим допуском (1e-1) почти все случайные векторы (реализованные в виде N-мерных массивов) были идентифицированы как уникальные. Я не наблюдал этого, используя vectors и sets.
  2. При включении флага оптимизации -O3 количество уникальных элементов значительно отличается от количества без оптимизации, и это наверняка говорит о том, что с моим подходом что-то не так.

Изменить: 2-я проблема решена с учетом замечания @WhozCraig.

Итак, мой вопрос: это странное поведение, потому что моя хэш-функция плохо спроектирована? Если да, не могли бы вы предложить, как улучшить хеш-функцию для моего случая?


person Remis    schedule 02.08.2016    source источник
comment
Вы проверили, каково фактическое значение хеш-функции данного массива? Изменится ли он при сборке с -O3? Вы смотрели на распределение хэш-значений для некоторого управляемого количества случайных массивов? Выбирает ли -O3 другой случайный движок по умолчанию?   -  person Useless    schedule 02.08.2016
comment
std::size_t ret; вы понимаете, что это неопределенно? Таким образом, последующий зацикленный ret += std::hash<double>()(elem); практически бесполезен. Так что да, я бы сказал, что он плохо спроектирован.   -  person WhozCraig    schedule 02.08.2016
comment
@WhozCraig Спасибо, это было действительно так (позор мне)! После инициализации std::size_t ret = 0 я избавился от 2-й странности.   -  person Remis    schedule 02.08.2016


Ответы (1)


Ваша программа демонстрирует неопределенное поведение (неинициализированная переменная std::size_t ret в стороне).

Во-первых, ArrayEqual не вызывает отношения эквивалентности. Это не транзитивно - существуют три массива a, b и c, такие что a "достаточно близко" к b и b "достаточно близко" к c, но a не "достаточно близко" к c.

Во-вторых, ArrayHash может не возвращать одно и то же значение хеш-функции для двух массивов, которые ArrayEqual объявляет равными.

Оба они являются обязательными для аргументов шаблона std::unordered_set.

person Igor Tandetnik    schedule 02.08.2016
comment
Спасибо, я полностью согласен с обоими вашими замечаниями. Практическая проблема сейчас состоит в том, как их исправить. - person Remis; 02.08.2016
comment
Вам нужно определить свои классы эквивалентности и придерживаться их. Например. вы можете разделить пространство на N-мерную сетку, 1e-6 единиц на стороне. Все точки, попадающие в одну и ту же ячейку, будут считаться эквивалентными на ArrayEqual. Выберите представителя класса (например, центр ячейки или угол с наименьшими координатами) и пусть ArrayHash вернет хэш этого представителя для каждой точки в ячейке. - person Igor Tandetnik; 02.08.2016