Re-implement AbslHashValue for std::vector<bool> and std::bitset to not simply defer to std::hash. This enables hash seed randomization (a better default behavior) at the cost of a modest slowdown for the rare case of hashing these types. One real world benchmark had a 17% slowdown. Users that need better performance can use std::hash or a custom data structure as the key which would likely be faster anyway. PiperOrigin-RevId: 949636074 Change-Id: I19b6dde7bb33427216157b5d61a0fa92da05a1a4
diff --git a/absl/hash/hash_benchmark.cc b/absl/hash/hash_benchmark.cc index d0ebdbc..86650b3 100644 --- a/absl/hash/hash_benchmark.cc +++ b/absl/hash/hash_benchmark.cc
@@ -13,6 +13,7 @@ // limitations under the License. #include <algorithm> +#include <bitset> #include <cassert> #include <cstddef> #include <cstdint> @@ -247,6 +248,12 @@ MAKE_BENCHMARK(AbslHash, VectorDouble_10, Vector<double>(10)); MAKE_BENCHMARK(AbslHash, VectorDouble_100, Vector<double>(100)); MAKE_BENCHMARK(AbslHash, VectorDouble_1000, Vector<double>(1000)); +MAKE_BENCHMARK(AbslHash, VectorBool_10, Vector<bool>(10)); +MAKE_BENCHMARK(AbslHash, VectorBool_100, Vector<bool>(100)); +MAKE_BENCHMARK(AbslHash, VectorBool_1000, Vector<bool>(1000)); +MAKE_BENCHMARK(AbslHash, Bitset_10, std::bitset<10>()); +MAKE_BENCHMARK(AbslHash, Bitset_100, std::bitset<100>()); +MAKE_BENCHMARK(AbslHash, Bitset_1000, std::bitset<1000>()); MAKE_BENCHMARK(AbslHash, FlatHashSetInt64_10, FlatHashSet<int64_t>(10)); MAKE_BENCHMARK(AbslHash, FlatHashSetInt64_100, FlatHashSet<int64_t>(100)); MAKE_BENCHMARK(AbslHash, FlatHashSetInt64_1000, FlatHashSet<int64_t>(1000)); @@ -294,6 +301,14 @@ std::vector<double>(100, 1.1)); MAKE_BENCHMARK(TypeErasedAbslHash, VectorDouble_1000, std::vector<double>(1000, 1.1)); +MAKE_BENCHMARK(TypeErasedAbslHash, VectorBool_10, std::vector<bool>(10, true)); +MAKE_BENCHMARK(TypeErasedAbslHash, VectorBool_100, + std::vector<bool>(100, true)); +MAKE_BENCHMARK(TypeErasedAbslHash, VectorBool_1000, + std::vector<bool>(1000, true)); +MAKE_BENCHMARK(TypeErasedAbslHash, Bitset_10, std::bitset<10>()); +MAKE_BENCHMARK(TypeErasedAbslHash, Bitset_100, std::bitset<100>()); +MAKE_BENCHMARK(TypeErasedAbslHash, Bitset_1000, std::bitset<1000>()); MAKE_BENCHMARK(TypeErasedAbslHash, FlatHashSetInt64_10, FlatHashSet<int64_t>(10)); MAKE_BENCHMARK(TypeErasedAbslHash, FlatHashSetInt64_100,
diff --git a/absl/hash/internal/hash.h b/absl/hash/internal/hash.h index f4633ed..f68872f 100644 --- a/absl/hash/internal/hash.h +++ b/absl/hash/internal/hash.h
@@ -787,41 +787,50 @@ } // AbslHashValue special cases for hashing std::vector<bool> - -#if defined(ABSL_IS_BIG_ENDIAN) && \ - (defined(__GLIBCXX__) || defined(__GLIBCPP__)) - -// std::hash in libstdc++ does not work correctly with vector<bool> on Big -// Endian platforms therefore we need to implement a custom AbslHashValue for -// it. More details on the bug: -// https://gcc.gnu.org/bugzilla/show_bug.cgi?id=102531 +// +// To achieve high performance without depending on private standard library +// internals, we pack bits 64 at a time into uint64_t words using a fixed +// 64-step inner loop that allows compilers to unroll bit shifts cleanly. +// +// This is slower than std::hash<std::vector<bool>> which can access private +// storage directly, but more than fast enough for the very rare case of hashing +// std::vector<bool>. In the event that higher performance is needed, a custom +// key type is likely faster than building std::vector<bool> and hashing it, +// otherwise users can just use std::hash as the hasher. template <typename H, typename T, typename Allocator> std::enable_if_t<is_hashable<T>::value && std::is_same_v<T, bool>, H> AbslHashValue(H hash_state, const std::vector<T, Allocator>& vector) { typename H::AbslInternalPiecewiseCombiner combiner; - for (const auto& i : vector) { - unsigned char c = static_cast<unsigned char>(i); - hash_state = combiner.add_buffer(std::move(hash_state), &c, sizeof(c)); + const size_t size = vector.size(); + size_t i = 0; + // Pack full 64-bit words. Fixed inner loop count enables compiler unrolling. + while (i + 64 <= size) { + uint64_t word = 0; + for (size_t j = 0; j < 64; ++j) { + word |= static_cast<uint64_t>(vector[i + j]) << j; + } + hash_state = combiner.add_buffer( + std::move(hash_state), reinterpret_cast<const unsigned char*>(&word), + sizeof(word)); + i += 64; } + // Pack remaining bits (< 64) into the final word. + if (i < size) { + uint64_t word = 0; + const size_t rem = size - i; + for (size_t j = 0; j < rem; ++j) { + word |= static_cast<uint64_t>(vector[i + j]) << j; + } + hash_state = combiner.add_buffer( + std::move(hash_state), reinterpret_cast<const unsigned char*>(&word), + (rem + 7) / 8); + } + // Mix in vector.size() to distinguish vectors with trailing false/zero bits + // (e.g. {true} vs {true, false}) that would otherwise produce identical bit + // buffers. return H::combine(combiner.finalize(std::move(hash_state)), - WeaklyMixedInteger{vector.size()}); + WeaklyMixedInteger{size}); } -#else -// When not working around the libstdc++ bug above, we still have to contend -// with the fact that std::hash<vector<bool>> is often poor quality, hashing -// directly on the internal words and on no other state. On these platforms, -// vector<bool>{1, 1} and vector<bool>{1, 1, 0} hash to the same value. -// -// Mixing in the size (as we do in our other vector<> implementations) on top -// of the library-provided hash implementation avoids this QOI issue. -template <typename H, typename T, typename Allocator> -std::enable_if_t<is_hashable<T>::value && std::is_same_v<T, bool>, H> -AbslHashValue(H hash_state, const std::vector<T, Allocator>& vector) { - return H::combine(std::move(hash_state), - std::hash<std::vector<T, Allocator>>{}(vector), - WeaklyMixedInteger{vector.size()}); -} -#endif // ----------------------------------------------------------------------------- // AbslHashValue for Ordered Associative Containers @@ -959,28 +968,44 @@ // AbslHashValue for Other Types // ----------------------------------------------------------------------------- -// AbslHashValue for hashing std::bitset is not defined on Little Endian -// platforms, for the same reason as for vector<bool> (see std::vector above): -// It does not expose the raw bytes, and a fallback to std::hash<> is most -// likely faster. - -#if defined(ABSL_IS_BIG_ENDIAN) && \ - (defined(__GLIBCXX__) || defined(__GLIBCPP__)) // AbslHashValue for hashing std::bitset // -// std::hash in libstdc++ does not work correctly with std::bitset on Big Endian -// platforms therefore we need to implement a custom AbslHashValue for it. More -// details on the bug: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=102531 +// To achieve high performance without depending on private standard library +// internals, we pack bits 64 at a time into uint64_t words using a fixed +// 64-step inner loop that allows compilers to unroll bit shifts cleanly. +// +// This is slower than std::hash<std::bitset> which can access private storage +// directly, but more than fast enough for the very rare case of hashing +// std::bitset. In the event that higher-performance is needed, users can just +// use std::hash as the hasher. template <typename H, size_t N> H AbslHashValue(H hash_state, const std::bitset<N>& set) { typename H::AbslInternalPiecewiseCombiner combiner; - for (size_t i = 0; i < N; i++) { - unsigned char c = static_cast<unsigned char>(set[i]); - hash_state = combiner.add_buffer(std::move(hash_state), &c, sizeof(c)); + size_t i = 0; + // Pack full 64-bit words. Fixed inner loop count enables compiler unrolling. + while (i + 64 <= N) { + uint64_t word = 0; + for (size_t j = 0; j < 64; ++j) { + word |= static_cast<uint64_t>(set[i + j]) << j; + } + hash_state = combiner.add_buffer( + std::move(hash_state), reinterpret_cast<const unsigned char*>(&word), + sizeof(word)); + i += 64; + } + // Pack remaining bits (< 64) into the final word. + if (i < N) { + uint64_t word = 0; + const size_t rem = N - i; + for (size_t j = 0; j < rem; ++j) { + word |= static_cast<uint64_t>(set[i + j]) << j; + } + hash_state = combiner.add_buffer( + std::move(hash_state), reinterpret_cast<const unsigned char*>(&word), + (rem + 7) / 8); } return H::combine(combiner.finalize(std::move(hash_state)), N); } -#endif // -----------------------------------------------------------------------------