2016-06-15 2 views
1

У меня очень странная проблема использования самоопределенной хэш-функции в std :: unordered_map.Hash a std :: array, который используется в std :: unordered_map

Мой ключ больше, чем int64, поэтому я использую std :: array для его представления. Чтобы получить его хэш-значение, я создаю класс MyHash:

class MyHash 
{ 
public: 
    std::size_t operator()(const std::array<char, 12>& oid) const 
    { 
     Convert t; 
     std::memcpy(t.arr, oid.data(), 12); 
     std::cout << t.a <<" "<<t.b << std::endl; 
     return (std::hash<std::int32_t>()(t.a)^(std::hash<std::int64_t>()(t.b) << 1)) >> 1; 
    } 
    union Convert { 
     struct { 
      std::int32_t a; 
      std::int64_t b; 
     }; 
     char arr[12]; 
    }; 
}; 

Первое, проверить:

std::array<char, 12> arr = {1,2,3,4,5,6,7,8,9,10,11,12}; 
MyHash o; 
o(arr); 
o(arr); 

Это нормально. Он печатает те же t.a и t.b. Теперь используйте его с станд :: unordered_map:

std::unordered_map<std::array<char, 12>, int, MyHash> map; 
std::array<char, 12> arr = {1,2,3,4,5,6,7,8,9,10,11,12}; 
map.insert(std::make_pair(arr, 1)); 
auto it = map.find(arr); 
if(it == map.end()) 
    std::cout << "error"; 
else 
    std::cout << it->second; 

Теперь он будет печатать error, причиной этого является t.b в вставке отличается с находкой. И это только произойдет в режиме выпуска против (или г ++ O2)

+0

Я предпочел бы вычислить хэш всех 12 байтов, используя, например, 'boost :: hash_range': http://coliru.stacked-crooked.com/a/cddb1ea79a18d0b1 –

+0

Сначала я использую его, но я нашел, что он немного медленный, потому что он будет делать 12-кратный хэш.И качество не очень хорошее из-за первых 4 байтов в массиве, почти такое же значение – jean

+0

, если первые 4 байта всегда одинаковы, вы можете просто пропустить их при вычислении хэша ('boost :: hash_range (oid.begin() +4, oid.end()); '); Вы измерили разницу во времени? насколько медленнее это, чем ваш подход memcpy/union? –

ответ

2

Чтобы избежать неопределенного поведения, упаковки и выравнивания проблем, вы можете скопировать отдельные целые числа:

#include <cstdint> 
#include <cstring> 
#include <array> 

std::size_t array_hash(const std::array<char, 12>& array) { 
    std::uint64_t u64; 
    std::memcpy(&u64, array.data(), 8); 
    std::uint32_t u32; 
    std::memcpy(&u32, array.data() + 8, 4); 
    // return (std::hash<std::uint32_t>()(u32)^(std::hash<std::uint64_t>()(u64) << 1)) >> 1;; 
    return u64 + u32; // for simplicity 
} 

std::size_t uint_hash(std::uint64_t u64, std::uint32_t u32) { 
    // return (std::hash<std::uint32_t>()(u32)^(std::hash<std::uint64_t>()(u64) << 1)) >> 1;; 
    return u64 + u32; // for simplicity 
} 

С (г ++ версии 4.8.4) г ++ -S --std = C++ 11 -O3 вы получите:

_Z10array_hashRKSt5arrayIcLm24EE: 
.LFB914: 
     .cfi_startproc 
     movl 8(%rdi), %eax 
     addq (%rdi), %rax 
     ret 
     .cfi_endproc 

и

_Z9uint_hashmj: 
.LFB915: 
     .cfi_startproc 
     movl %esi, %eax 
     addq %rdi, %rax 
     ret 
     .cfi_endproc 

... который является довольно оптимальной.

Смотрите также: Type Punning, Strict Aliasing, and Optimization

+0

+1 за то, что вы пытаетесь избежать UB и ссылаетесь на предупреждение о том, насколько волатильно это все. Я рад, что дошел до того, что просмотр 'reinterpret_cast' заставляет меня вздрогнуть, и я знаю, как использовать memcpy, если есть неопределенность _any_. Кроме того, как вы показали, часто компилятор оптимизирует «memcpy» в том, что вы ожидаете от 'reinterpret_cast' ... но с гарантированным определенным поведением первого, в отличие от последнего. Лучшее из обоих миров! –

1

Давайте посмотрим на этот

union Convert { 
     struct { 
      std::int32_t a; 
      std::int64_t b; 
     }; 
     char arr[12]; 
    }; 

Компилятор может также пакет дополнительные байты между a и b. Таким образом, тип, выполняющий кастинг через массив char, не обязательно будет накладывать часть struct. Тип punning также является пограничным неопределенным поведением в C++; хотя я думаю вы в порядке в этом конкретном случае.

Похоже, что компоновки упаковки для сборки выпуска отличаются от сборки отладки.

Многие компиляторы позволяют указать устройства упаковки (#pragma pack?), Но я бы не стал полагаться на это, если бы я был вами, поскольку он побеждает стратегии оптимизации компилятора и также по существу нестандартный C++.

+0

Подкачка порядка 'a' и' b' удалит прокладку между ними, потому что 64-битный int должен быть выровнен по 8 байт, тогда как 32-битный int должен быть выровнен по 4 байт. –

+0

Возможно, это не так. Особенно на 128-битном чипе. – Bathsheba

+0

Заполнение макета памяти не должно решаться во время компиляции? Массив char может не полностью перекрывать a и b, но почему он изменится во время выполнения? И это происходит только тогда, когда вы используете его как unordered_map хэш-объект? – jean

0

Это немного рубить, но вы можете попробовать и посмотреть, как это работает:

struct MyHash { 
    std::size_t operator()(const std::array<char, 12>& oid) const { 
     auto d = reinterpret_cast<const std::uint32_t*>(oid.data()); 
     std::size_t prime = 31; 
     std::size_t other_prime = 59; 
     return d[2] + other_prime*(d[1] + prime*d[0]); 
    } 
}; 

Это работает только потому, что 12 кратно sizeof(uint32_t) виду вас. Если изменения размера вам придется корректировать.

Смежные вопросы