Ultra 0.9.4
Loading...
Searching...
No Matches
hash_t.h
Go to the documentation of this file.
1
12
13#if !defined(ULTRA_HASH_T_H)
14#define ULTRA_HASH_T_H
15
16#include "utility/assert.h"
17
18#include <algorithm>
19#include <array>
20#include <bit>
21#include <concepts>
22#include <cstdint>
23#include <cstring>
24#include <iostream>
25#include <span>
26
27namespace ultra
28{
33struct hash_t
34{
35 explicit hash_t(std::uint64_t a = 0, std::uint64_t b = 0) : data{a, b} {}
36
38 void clear() noexcept { data[0] = data[1] = 0; }
39
41 [[nodiscard]] bool operator==(const hash_t &) const noexcept = default;
42
43 [[nodiscard]] bool operator<(const hash_t &rhs) const noexcept
44 {
45 return data[0] < rhs.data[0]
46 || (data[0] == rhs.data[0] && data[1] < rhs.data[1]);
47 }
48
65 void combine(hash_t h) noexcept
66 {
67 // This is the simple algorithm used in `Apache.Commons.HashCodeBuilder`.
68 // It uses simple prime number multiplication and is a special case of
69 // Bob Jenkins' idea (`m * H(A) + H(B)`).
70 data[0] = data[0] * 37 + h.data[0];
71 data[1] = data[1] * 37 + h.data[1];
72
73 // An alternative from Boost is:
74 // data[0] ^= h.data[0] + 0x9e3779b9 + (data[0] << 6) + (data[0] >> 2);
75 // data[1] ^= h.data[1] + 0x9e3779b9 + (data[1] << 6) + (data[1] >> 2);
76 // Under Boost, combining {0, 0} into {0, 0} immediately produces 0x9e3779b9
77 // (non-empty).
78
79 // In spite of its handy bit-mixing properties, XOR is not a good way to
80 // combine hashes due to its commutativity (e.g. see
81 // https://stackoverflow.com/q/5889238/3235496).
82 }
83
85 [[nodiscard]] bool empty() const noexcept { return !data[0] && !data[1]; }
86
87 // Serialization.
88 bool load(std::istream &);
89 bool save(std::ostream &) const;
90
91 // Data members.
92 std::uint64_t data[2];
93};
94
95std::ostream &operator<<(std::ostream &, hash_t);
96
97// Implementation details for MurmurHash3 x64 128-bit hashing.
98// This namespace contains the low-level primitives shared by both the
99// one-shot (`murmurhash3::hash128`) and incremental (`murmurhash3_sink`)
100// implementations: block mixing, tail processing, finalisation.
101// Keeping these routines in a single place guarantees bitwise equivalence
102// between hashing modes and avoids code duplication.
103namespace internal
104{
105
106// For MurmurHash3 x64 128-bit, the block size is 16 bytes, i.e. 128 bits.
107inline constexpr std::size_t murmurhash3_block_size {16};
108
109static constexpr std::uint64_t c1 {0x87c37b91114253d5};
110static constexpr std::uint64_t c2 {0x4cf5ad432745937f};
111
112inline std::uint64_t fmix(std::uint64_t k) noexcept
113{
114 // The constants were generated by a simple simulated-annealing algorithm.
115 k ^= k >> 33;
116 k *= 0xff51afd7ed558ccd;
117 k ^= k >> 33;
118 k *= 0xc4ceb9fe1a85ec53;
119 k ^= k >> 33;
120
121 return k;
122}
123
124inline std::uint32_t fmix(std::uint32_t k) noexcept
125{
126 // The constants were generated by a simple simulated-annealing algorithm.
127 k ^= k >> 16;
128 k *= 0x85ebca6b;
129 k ^= k >> 13;
130 k *= 0xc2b2ae35;
131 k ^= k >> 16;
132
133 return k;
134}
135
145inline void finalize(hash_t &h, std::size_t total_len) noexcept
146{
147 h.data[0] ^= total_len;
148 h.data[1] ^= total_len;
149
150 h.data[0] += h.data[1];
151 h.data[1] += h.data[0];
152
153 h.data[0] = fmix(h.data[0]);
154 h.data[1] = fmix(h.data[1]);
155
156 h.data[0] += h.data[1];
157 h.data[1] += h.data[0];
158}
159
170inline void process_block(hash_t &h, std::uint64_t k1,
171 std::uint64_t k2) noexcept
172{
173 k1 *= c1;
174 k1 = std::rotl(k1, 31);
175 k1 *= c2;
176 h.data[0] ^= k1;
177
178 h.data[0] = std::rotl(h.data[0], 27);
179 h.data[0] += h.data[1];
180 h.data[0] = h.data[0] * 5 + 0x52dce729;
181
182 k2 *= c2;
183 k2 = std::rotl(k2, 33);
184 k2 *= c1;
185 h.data[1] ^= k2;
186
187 h.data[1] = std::rotl(h.data[1], 31);
188 h.data[1] += h.data[0];
189 h.data[1] = h.data[1] * 5 + 0x38495ab5;
190}
191
202inline void process_tail(hash_t &h,
203 const std::byte *tail, std::size_t len) noexcept
204{
205 std::uint64_t k1(0), k2(0);
206
207 switch (len)
208 {
209 case 15: k2 ^= std::uint64_t(tail[14]) << 48; [[fallthrough]];
210 case 14: k2 ^= std::uint64_t(tail[13]) << 40; [[fallthrough]];
211 case 13: k2 ^= std::uint64_t(tail[12]) << 32; [[fallthrough]];
212 case 12: k2 ^= std::uint64_t(tail[11]) << 24; [[fallthrough]];
213 case 11: k2 ^= std::uint64_t(tail[10]) << 16; [[fallthrough]];
214 case 10: k2 ^= std::uint64_t(tail[ 9]) << 8; [[fallthrough]];
215 case 9: k2 ^= std::uint64_t(tail[ 8]);
216 k2 *= c2; k2 = std::rotl(k2, 33); k2 *= c1; h.data[1] ^= k2;
217 [[fallthrough]];
218 case 8: k1 ^= std::uint64_t(tail[ 7]) << 56; [[fallthrough]];
219 case 7: k1 ^= std::uint64_t(tail[ 6]) << 48; [[fallthrough]];
220 case 6: k1 ^= std::uint64_t(tail[ 5]) << 40; [[fallthrough]];
221 case 5: k1 ^= std::uint64_t(tail[ 4]) << 32; [[fallthrough]];
222 case 4: k1 ^= std::uint64_t(tail[ 3]) << 24; [[fallthrough]];
223 case 3: k1 ^= std::uint64_t(tail[ 2]) << 16; [[fallthrough]];
224 case 2: k1 ^= std::uint64_t(tail[ 1]) << 8; [[fallthrough]];
225 case 1: k1 ^= std::uint64_t(tail[ 0]);
226 k1 *= c1; k1 = std::rotl(k1, 31); k1 *= c2;
227 h.data[0] ^= k1;
228 }
229}
230
231template<std::unsigned_integral T>
232[[nodiscard]] T load_unaligned(const std::byte *p) noexcept
233{
234 // Loads using the host byte order. This is well-defined, but hash values are
235 // not guaranteed to be identical across endian architectures.
236 //
237 // Use memcpy() to load potentially unaligned bytes without violating
238 // strict-aliasing rules.
239
240 T tmp;
241 std::memcpy(&tmp, p, sizeof(T));
242 return tmp;
243}
244
245} // namespace internal
246
259{
260public:
261 [[nodiscard]] static hash_t hash128(const void *const, std::size_t,
262 std::uint32_t = 1973) noexcept;
263};
264
273inline hash_t murmurhash3::hash128(const void *const data, std::size_t len,
274 std::uint32_t seed) noexcept
275{
276 Expects(data || len == 0);
277
278 const auto n_blocks(len / internal::murmurhash3_block_size);
279
280 // Body.
281 const auto *bytes(reinterpret_cast<const std::byte *>(data));
282
283 hash_t h(seed, seed);
284
285 using internal::load_unaligned;
286 for (std::size_t i(0); i < n_blocks; ++i)
287 {
288 const auto offset(i * internal::murmurhash3_block_size);
289
290 const auto k1(load_unaligned<std::uint64_t>(bytes + offset));
291 const auto k2(load_unaligned<std::uint64_t>(bytes + offset + sizeof(k1)));
292
293 internal::process_block(h, k1, k2);
294 }
295
296 // Tail.
297 if (const auto tail_len(len % internal::murmurhash3_block_size); tail_len)
298 {
299 const auto tail_offset(len - tail_len);
300 internal::process_tail(h, bytes + tail_offset, tail_len);
301 }
302
303 internal::finalize(h, len);
304
305 return h;
306}
307
319{
320public:
321 explicit murmurhash3_sink(std::uint32_t = 1973) noexcept;
322
323 void write(std::span<const std::byte>) noexcept;
324 [[nodiscard]] hash_t final() noexcept;
325
326private:
327 void process_block(const std::byte* data) noexcept;
328
329private:
330 hash_t h_;
331 std::array<std::byte, internal::murmurhash3_block_size> tail_ {};
332 std::size_t tail_size_ {0};
333 std::size_t total_len_ {0};
334}; // class murmurhash3_sink
335
341inline murmurhash3_sink::murmurhash3_sink(std::uint32_t seed) noexcept
342 : h_(seed, seed)
343{
344}
345
355inline void murmurhash3_sink::write(std::span<const std::byte> bytes) noexcept
356{
357 total_len_ += bytes.size();
358
359 // Fill tail if needed.
360 if (tail_size_)
361 {
362 const auto n(std::min<std::size_t>(
363 internal::murmurhash3_block_size - tail_size_,
364 bytes.size()));
365
366 std::memcpy(tail_.data() + tail_size_, bytes.data(), n);
367 tail_size_ += n;
368 bytes = bytes.subspan(n);
369
370 if (tail_size_ == internal::murmurhash3_block_size)
371 {
372 process_block(tail_.data());
373 tail_size_ = 0;
374 }
375 }
376
377 // Process full blocks.
378 while (bytes.size() >= internal::murmurhash3_block_size)
379 {
380 process_block(bytes.data());
381 bytes = bytes.subspan(internal::murmurhash3_block_size);
382 }
383
384 // Store remaining bytes as tail.
385 if (!bytes.empty())
386 {
387 std::memcpy(tail_.data(), bytes.data(), bytes.size());
388 tail_size_ = bytes.size();
389 }
390}
391
392inline void murmurhash3_sink::process_block(const std::byte *data) noexcept
393{
394 const auto k1(internal::load_unaligned<std::uint64_t>(data));
395 const auto k2(internal::load_unaligned<std::uint64_t>(data + sizeof(k1)));
396
397 internal::process_block(h_, k1, k2);
398}
399
410{
411 internal::process_tail(h_, reinterpret_cast<const std::byte *>(tail_.data()),
412 tail_size_);
413 internal::finalize(h_, total_len_);
414 return h_;
415}
416
417using hash = murmurhash3;
418using hash_sink = murmurhash3_sink;
419
424template <class T>
425requires (std::integral<T> || std::floating_point<T>)
426[[nodiscard]] std::span<const std::byte, sizeof(T)>
427bytes_view(const T &t) noexcept
428{
429 return std::span<const std::byte, sizeof(T)>{
430 reinterpret_cast<const std::byte *>(std::addressof(t)), sizeof(T)};
431}
432
433[[nodiscard]] std::span<const std::byte>
434bytes_view(const std::string &) noexcept;
435
436} // namespace ultra
437
438#endif // include guard
Definition hash_t.h:319
void write(std::span< const std::byte >) noexcept
Definition hash_t.h:355
murmurhash3_sink(std::uint32_t=1973) noexcept
Definition hash_t.h:341
hash_t final() noexcept
Definition hash_t.h:409
Definition hash_t.h:259
static hash_t hash128(const void *const, std::size_t, std::uint32_t=1973) noexcept
Definition hash_t.h:273
void process_tail(hash_t &h, const std::byte *tail, std::size_t len) noexcept
Definition hash_t.h:202
void process_block(hash_t &h, std::uint64_t k1, std::uint64_t k2) noexcept
Definition hash_t.h:170
void finalize(hash_t &h, std::size_t total_len) noexcept
Definition hash_t.h:145
std::span< const std::byte > bytes_view(const std::string &s) noexcept
Definition hash_t.cc:63
Definition hash_t.h:34
bool empty() const noexcept
We assume that a string of 128 zero bits means empty.
Definition hash_t.h:85
void clear() noexcept
Resets the content of the object.
Definition hash_t.h:38
void combine(hash_t h) noexcept
Definition hash_t.h:65
bool load(std::istream &)
Definition hash_t.cc:24
bool save(std::ostream &) const
Definition hash_t.cc:40
bool operator==(const hash_t &) const noexcept=default
Standard equality operator for hash signature.