authorgravatar for marc@tiehu.isMarc Tiehuis <marc@tiehu.is> 2023-06-08 14:27:17+12:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2023-06-07 19:27:17-07:00
log00ff65357666a7ec161d6e80c92be2ccc79e3638
tree7d15df0008aa90f0ce89906929f70bc4e576b2ec
parentcb7040154e7c9a381951d0f5d969a9dd97be1ec0
signaturebadge-question-mark Signed by PGP key 4AEE18F83AFDEB23

reimplement wyhash v4.1 (#15969)


2 files changed, 192 insertions(+), 161 deletions(-)

lib/std/hash/auto_hash.zig+1-1
......@@ -343,7 +343,7 @@ test "testHash optional" {
343343 const b: ?u32 = null;
344344 try testing.expectEqual(testHash(a), testHash(@as(u32, 123)));
345345 try testing.expect(testHash(a) != testHash(b));
346 try testing.expectEqual(testHash(b), 0);
346 try testing.expectEqual(testHash(b), 0x409638ee2bde459); // wyhash empty input hash
347347}
348348
349349test "testHash array" {
lib/std/hash/wyhash.zig+191-160
......@@ -1,209 +1,240 @@
11const std = @import("std");
2const mem = std.mem;
3
4const primes = [_]u64{
5 0xa0761d6478bd642f,
6 0xe7037ed1a0b428db,
7 0x8ebc6af09c88c6e3,
8 0x589965cc75374cc3,
9 0x1d8e4e27c47d124f,
10};
112
12fn read_bytes(comptime bytes: u8, data: []const u8) u64 {
13 const T = std.meta.Int(.unsigned, 8 * bytes);
14 return mem.readIntLittle(T, data[0..bytes]);
15}
3pub const Wyhash = struct {
4 const secret = [_]u64{
5 0xa0761d6478bd642f,
6 0xe7037ed1a0b428db,
7 0x8ebc6af09c88c6e3,
8 0x589965cc75374cc3,
9 };
10
11 a: u64,
12 b: u64,
13 state: [3]u64,
14 total_len: usize,
15
16 buf: [48]u8,
17 buf_len: usize,
1618
17fn read_8bytes_swapped(data: []const u8) u64 {
18 return (read_bytes(4, data) << 32 | read_bytes(4, data[4..]));
19}
19 pub fn init(seed: u64) Wyhash {
20 var self = Wyhash{
21 .a = undefined,
22 .b = undefined,
23 .state = undefined,
24 .total_len = 0,
25 .buf = undefined,
26 .buf_len = 0,
27 };
2028
21fn mum(a: u64, b: u64) u64 {
22 var r = std.math.mulWide(u64, a, b);
23 r = (r >> 64) ^ r;
24 return @truncate(u64, r);
25}
29 self.state[0] = seed ^ mix(seed ^ secret[0], secret[1]);
30 self.state[1] = self.state[0];
31 self.state[2] = self.state[0];
32 return self;
33 }
2634
27fn mix0(a: u64, b: u64, seed: u64) u64 {
28 return mum(a ^ seed ^ primes[0], b ^ seed ^ primes[1]);
29}
35 // This is subtly different from other hash function update calls. Wyhash requires the last
36 // full 48-byte block to be run through final1 if is exactly aligned to 48-bytes.
37 pub fn update(self: *Wyhash, input: []const u8) void {
38 self.total_len += input.len;
3039
31fn mix1(a: u64, b: u64, seed: u64) u64 {
32 return mum(a ^ seed ^ primes[2], b ^ seed ^ primes[3]);
33}
40 if (input.len <= 48 - self.buf_len) {
41 @memcpy(self.buf[self.buf_len..][0..input.len], input);
42 self.buf_len += input.len;
43 return;
44 }
3445
35// Wyhash version which does not store internal state for handling partial buffers.
36// This is needed so that we can maximize the speed for the short key case, which will
37// use the non-iterative api which the public Wyhash exposes.
38const WyhashStateless = struct {
39 seed: u64,
40 msg_len: usize,
46 var i: usize = 0;
4147
42 pub fn init(seed: u64) WyhashStateless {
43 return WyhashStateless{
44 .seed = seed,
45 .msg_len = 0,
46 };
47 }
48 if (self.buf_len > 0) {
49 i = 48 - self.buf_len;
50 @memcpy(self.buf[self.buf_len..][0..i], input[0..i]);
51 self.round(&self.buf);
52 self.buf_len = 0;
53 }
4854
49 fn round(self: *WyhashStateless, b: []const u8) void {
50 std.debug.assert(b.len == 32);
55 while (i + 48 < input.len) : (i += 48) {
56 self.round(input[i..][0..48]);
57 }
5158
52 self.seed = mix0(
53 read_bytes(8, b[0..]),
54 read_bytes(8, b[8..]),
55 self.seed,
56 ) ^ mix1(
57 read_bytes(8, b[16..]),
58 read_bytes(8, b[24..]),
59 self.seed,
60 );
59 const remaining_bytes = input[i..];
60 @memcpy(self.buf[0..remaining_bytes.len], remaining_bytes);
61 self.buf_len = remaining_bytes.len;
6162 }
6263
63 pub fn update(self: *WyhashStateless, b: []const u8) void {
64 std.debug.assert(b.len % 32 == 0);
65
66 var off: usize = 0;
67 while (off < b.len) : (off += 32) {
68 @call(.always_inline, round, .{ self, b[off..][0..32] });
64 pub fn final(self: *Wyhash) u64 {
65 var input = self.buf[0..self.buf_len];
66 var newSelf = self.shallowCopy(); // ensure idempotency
67
68 if (self.total_len <= 16) {
69 newSelf.smallKey(input);
70 } else {
71 if (self.buf_len < 16) {
72 var scratch: [16]u8 = undefined;
73 const rem = 16 - self.buf_len;
74 @memcpy(scratch[0..rem], self.buf[self.buf.len - rem ..][0..rem]);
75 @memcpy(scratch[rem..][0..self.buf_len], self.buf[0..self.buf_len]);
76
77 // Same as input with lookbehind to pad to 16-bytes
78 input = scratch[rem..];
79 }
80
81 newSelf.final0();
82 newSelf.final1(input);
6983 }
7084
71 self.msg_len += b.len;
72 }
73
74 pub fn final(self: *WyhashStateless, b: []const u8) u64 {
75 std.debug.assert(b.len < 32);
76
77 const seed = self.seed;
78 const rem_len = @intCast(u5, b.len);
79 const rem_key = b[0..rem_len];
80
81 self.seed = switch (rem_len) {
82 0 => seed,
83 1 => mix0(read_bytes(1, rem_key), primes[4], seed),
84 2 => mix0(read_bytes(2, rem_key), primes[4], seed),
85 3 => mix0((read_bytes(2, rem_key) << 8) | read_bytes(1, rem_key[2..]), primes[4], seed),
86 4 => mix0(read_bytes(4, rem_key), primes[4], seed),
87 5 => mix0((read_bytes(4, rem_key) << 8) | read_bytes(1, rem_key[4..]), primes[4], seed),
88 6 => mix0((read_bytes(4, rem_key) << 16) | read_bytes(2, rem_key[4..]), primes[4], seed),
89 7 => mix0((read_bytes(4, rem_key) << 24) | (read_bytes(2, rem_key[4..]) << 8) | read_bytes(1, rem_key[6..]), primes[4], seed),
90 8 => mix0(read_8bytes_swapped(rem_key), primes[4], seed),
91 9 => mix0(read_8bytes_swapped(rem_key), read_bytes(1, rem_key[8..]), seed),
92 10 => mix0(read_8bytes_swapped(rem_key), read_bytes(2, rem_key[8..]), seed),
93 11 => mix0(read_8bytes_swapped(rem_key), (read_bytes(2, rem_key[8..]) << 8) | read_bytes(1, rem_key[10..]), seed),
94 12 => mix0(read_8bytes_swapped(rem_key), read_bytes(4, rem_key[8..]), seed),
95 13 => mix0(read_8bytes_swapped(rem_key), (read_bytes(4, rem_key[8..]) << 8) | read_bytes(1, rem_key[12..]), seed),
96 14 => mix0(read_8bytes_swapped(rem_key), (read_bytes(4, rem_key[8..]) << 16) | read_bytes(2, rem_key[12..]), seed),
97 15 => mix0(read_8bytes_swapped(rem_key), (read_bytes(4, rem_key[8..]) << 24) | (read_bytes(2, rem_key[12..]) << 8) | read_bytes(1, rem_key[14..]), seed),
98 16 => mix0(read_8bytes_swapped(rem_key), read_8bytes_swapped(rem_key[8..]), seed),
99 17 => mix0(read_8bytes_swapped(rem_key), read_8bytes_swapped(rem_key[8..]), seed) ^ mix1(read_bytes(1, rem_key[16..]), primes[4], seed),
100 18 => mix0(read_8bytes_swapped(rem_key), read_8bytes_swapped(rem_key[8..]), seed) ^ mix1(read_bytes(2, rem_key[16..]), primes[4], seed),
101 19 => mix0(read_8bytes_swapped(rem_key), read_8bytes_swapped(rem_key[8..]), seed) ^ mix1((read_bytes(2, rem_key[16..]) << 8) | read_bytes(1, rem_key[18..]), primes[4], seed),
102 20 => mix0(read_8bytes_swapped(rem_key), read_8bytes_swapped(rem_key[8..]), seed) ^ mix1(read_bytes(4, rem_key[16..]), primes[4], seed),
103 21 => mix0(read_8bytes_swapped(rem_key), read_8bytes_swapped(rem_key[8..]), seed) ^ mix1((read_bytes(4, rem_key[16..]) << 8) | read_bytes(1, rem_key[20..]), primes[4], seed),
104 22 => mix0(read_8bytes_swapped(rem_key), read_8bytes_swapped(rem_key[8..]), seed) ^ mix1((read_bytes(4, rem_key[16..]) << 16) | read_bytes(2, rem_key[20..]), primes[4], seed),
105 23 => mix0(read_8bytes_swapped(rem_key), read_8bytes_swapped(rem_key[8..]), seed) ^ mix1((read_bytes(4, rem_key[16..]) << 24) | (read_bytes(2, rem_key[20..]) << 8) | read_bytes(1, rem_key[22..]), primes[4], seed),
106 24 => mix0(read_8bytes_swapped(rem_key), read_8bytes_swapped(rem_key[8..]), seed) ^ mix1(read_8bytes_swapped(rem_key[16..]), primes[4], seed),
107 25 => mix0(read_8bytes_swapped(rem_key), read_8bytes_swapped(rem_key[8..]), seed) ^ mix1(read_8bytes_swapped(rem_key[16..]), read_bytes(1, rem_key[24..]), seed),
108 26 => mix0(read_8bytes_swapped(rem_key), read_8bytes_swapped(rem_key[8..]), seed) ^ mix1(read_8bytes_swapped(rem_key[16..]), read_bytes(2, rem_key[24..]), seed),
109 27 => mix0(read_8bytes_swapped(rem_key), read_8bytes_swapped(rem_key[8..]), seed) ^ mix1(read_8bytes_swapped(rem_key[16..]), (read_bytes(2, rem_key[24..]) << 8) | read_bytes(1, rem_key[26..]), seed),
110 28 => mix0(read_8bytes_swapped(rem_key), read_8bytes_swapped(rem_key[8..]), seed) ^ mix1(read_8bytes_swapped(rem_key[16..]), read_bytes(4, rem_key[24..]), seed),
111 29 => mix0(read_8bytes_swapped(rem_key), read_8bytes_swapped(rem_key[8..]), seed) ^ mix1(read_8bytes_swapped(rem_key[16..]), (read_bytes(4, rem_key[24..]) << 8) | read_bytes(1, rem_key[28..]), seed),
112 30 => mix0(read_8bytes_swapped(rem_key), read_8bytes_swapped(rem_key[8..]), seed) ^ mix1(read_8bytes_swapped(rem_key[16..]), (read_bytes(4, rem_key[24..]) << 16) | read_bytes(2, rem_key[28..]), seed),
113 31 => mix0(read_8bytes_swapped(rem_key), read_8bytes_swapped(rem_key[8..]), seed) ^ mix1(read_8bytes_swapped(rem_key[16..]), (read_bytes(4, rem_key[24..]) << 24) | (read_bytes(2, rem_key[28..]) << 8) | read_bytes(1, rem_key[30..]), seed),
85 return newSelf.final2();
86 }
87
88 // Copies the core wyhash state but not any internal buffers.
89 inline fn shallowCopy(self: *Wyhash) Wyhash {
90 return .{
91 .a = self.a,
92 .b = self.b,
93 .state = self.state,
94 .total_len = self.total_len,
95 .buf = undefined,
96 .buf_len = undefined,
11497 };
98 }
11599
116 self.msg_len += b.len;
117 return mum(self.seed ^ self.msg_len, primes[4]);
100 inline fn smallKey(self: *Wyhash, input: []const u8) void {
101 std.debug.assert(input.len <= 16);
102
103 if (input.len >= 4) {
104 const end = input.len - 4;
105 const quarter = (input.len >> 3) << 2;
106 self.a = (read(4, input[0..]) << 32) | read(4, input[quarter..]);
107 self.b = (read(4, input[end..]) << 32) | read(4, input[end - quarter ..]);
108 } else if (input.len > 0) {
109 self.a = (@as(u64, input[0]) << 16) | (@as(u64, input[input.len >> 1]) << 8) | input[input.len - 1];
110 self.b = 0;
111 } else {
112 self.a = 0;
113 self.b = 0;
114 }
118115 }
119116
120 pub fn hash(seed: u64, input: []const u8) u64 {
121 const aligned_len = input.len - (input.len % 32);
117 inline fn round(self: *Wyhash, input: *const [48]u8) void {
118 inline for (0..3) |i| {
119 const a = read(8, input[8 * (2 * i) ..]);
120 const b = read(8, input[8 * (2 * i + 1) ..]);
121 self.state[i] = mix(a ^ secret[i + 1], b ^ self.state[i]);
122 }
123 }
122124
123 var c = WyhashStateless.init(seed);
124 @call(.always_inline, update, .{ &c, input[0..aligned_len] });
125 return @call(.always_inline, final, .{ &c, input[aligned_len..] });
125 inline fn read(comptime bytes: usize, data: []const u8) u64 {
126 std.debug.assert(bytes <= 8);
127 const T = std.meta.Int(.unsigned, 8 * bytes);
128 return @as(u64, std.mem.readIntLittle(T, data[0..bytes]));
126129 }
127};
128130
129/// Fast non-cryptographic 64bit hash function.
130/// See https://github.com/wangyi-fudan/wyhash
131pub const Wyhash = struct {
132 state: WyhashStateless,
131 inline fn mum(a: *u64, b: *u64) void {
132 const x = @as(u128, a.*) *% b.*;
133 a.* = @truncate(u64, x);
134 b.* = @truncate(u64, x >> 64);
135 }
133136
134 buf: [32]u8,
135 buf_len: usize,
137 inline fn mix(a_: u64, b_: u64) u64 {
138 var a = a_;
139 var b = b_;
140 mum(&a, &b);
141 return a ^ b;
142 }
136143
137 pub fn init(seed: u64) Wyhash {
138 return Wyhash{
139 .state = WyhashStateless.init(seed),
140 .buf = undefined,
141 .buf_len = 0,
142 };
144 inline fn final0(self: *Wyhash) void {
145 self.state[0] ^= self.state[1] ^ self.state[2];
143146 }
144147
145 pub fn update(self: *Wyhash, b: []const u8) void {
146 var off: usize = 0;
148 // Input must reside in a 16-byte buffer. The input slice passed be offset into it in which
149 // case this function will index in front of the slice.
150 inline fn final1(self: *Wyhash, input: []const u8) void {
151 std.debug.assert(input.len <= 48);
147152
148 if (self.buf_len != 0 and self.buf_len + b.len >= 32) {
149 off += 32 - self.buf_len;
150 @memcpy(self.buf[self.buf_len..][0..off], b[0..off]);
151 self.state.update(self.buf[0..]);
152 self.buf_len = 0;
153 var i: usize = 0;
154 while (i + 16 < input.len) : (i += 16) {
155 self.state[0] = mix(read(8, input[i..]) ^ secret[1], read(8, input[i + 8 ..]) ^ self.state[0]);
153156 }
154157
155 const remain_len = b.len - off;
156 const aligned_len = remain_len - (remain_len % 32);
157 self.state.update(b[off .. off + aligned_len]);
158
159 const src = b[off + aligned_len ..];
160 @memcpy(self.buf[self.buf_len..][0..src.len], src);
161 self.buf_len += @intCast(u8, b[off + aligned_len ..].len);
158 // Possible lookbehind past pointer start.
159 self.a = read(8, (input.ptr + input.len - 16)[0..8]);
160 self.b = read(8, (input.ptr + input.len - 8)[0..8]);
162161 }
163162
164 pub fn final(self: *Wyhash) u64 {
165 const rem_key = self.buf[0..self.buf_len];
166
167 return self.state.final(rem_key);
163 inline fn final2(self: *Wyhash) u64 {
164 self.a ^= secret[1];
165 self.b ^= self.state[0];
166 mum(&self.a, &self.b);
167 return mix(self.a ^ secret[0] ^ self.total_len, self.b ^ secret[1]);
168168 }
169169
170170 pub fn hash(seed: u64, input: []const u8) u64 {
171 return WyhashStateless.hash(seed, input);
171 var self = Wyhash.init(seed);
172
173 if (input.len <= 16) {
174 self.smallKey(input);
175 } else {
176 var i: usize = 0;
177 if (input.len >= 48) {
178 while (i + 48 < input.len) : (i += 48) {
179 self.round(input[i..][0..48]);
180 }
181 self.final0();
182 }
183 self.final1(input[i..]);
184 }
185
186 self.total_len = input.len;
187 return self.final2();
172188 }
173189};
174190
175191const expectEqual = std.testing.expectEqual;
176192
193const TestVector = struct {
194 expected: u64,
195 seed: u64,
196 input: []const u8,
197};
198
199// Run https://github.com/wangyi-fudan/wyhash/blob/77e50f267fbc7b8e2d09f2d455219adb70ad4749/test_vector.cpp directly.
200const vectors = [_]TestVector{
201 .{ .seed = 0, .expected = 0x409638ee2bde459, .input = "" },
202 .{ .seed = 1, .expected = 0xa8412d091b5fe0a9, .input = "a" },
203 .{ .seed = 2, .expected = 0x32dd92e4b2915153, .input = "abc" },
204 .{ .seed = 3, .expected = 0x8619124089a3a16b, .input = "message digest" },
205 .{ .seed = 4, .expected = 0x7a43afb61d7f5f40, .input = "abcdefghijklmnopqrstuvwxyz" },
206 .{ .seed = 5, .expected = 0xff42329b90e50d58, .input = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" },
207 .{ .seed = 6, .expected = 0xc39cab13b115aad3, .input = "12345678901234567890123456789012345678901234567890123456789012345678901234567890" },
208};
209
177210test "test vectors" {
178 const hash = Wyhash.hash;
179
180 try expectEqual(hash(0, ""), 0x0);
181 try expectEqual(hash(1, "a"), 0xbed235177f41d328);
182 try expectEqual(hash(2, "abc"), 0xbe348debe59b27c3);
183 try expectEqual(hash(3, "message digest"), 0x37320f657213a290);
184 try expectEqual(hash(4, "abcdefghijklmnopqrstuvwxyz"), 0xd0b270e1d8a7019c);
185 try expectEqual(hash(5, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"), 0x602a1894d3bbfe7f);
186 try expectEqual(hash(6, "12345678901234567890123456789012345678901234567890123456789012345678901234567890"), 0x829e9c148b75970e);
211 for (vectors) |e| {
212 try expectEqual(e.expected, Wyhash.hash(e.seed, e.input));
213 }
187214}
188215
189216test "test vectors streaming" {
190 var wh = Wyhash.init(5);
191 for ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") |e| {
192 wh.update(mem.asBytes(&e));
217 const step = 5;
218
219 for (vectors) |e| {
220 var wh = Wyhash.init(e.seed);
221 var i: usize = 0;
222 while (i < e.input.len) : (i += step) {
223 const len = if (i + step > e.input.len) e.input.len - i else step;
224 wh.update(e.input[i..][0..len]);
225 }
226 try expectEqual(e.expected, wh.final());
193227 }
194 try expectEqual(wh.final(), 0x602a1894d3bbfe7f);
228}
195229
196 const pattern = "1234567890";
197 const count = 8;
198 const result = 0x829e9c148b75970e;
199 try expectEqual(Wyhash.hash(6, pattern ** 8), result);
230test "test ensure idempotent final call" {
231 const e: TestVector = .{ .seed = 6, .expected = 0xc39cab13b115aad3, .input = "12345678901234567890123456789012345678901234567890123456789012345678901234567890" };
232 var wh = Wyhash.init(e.seed);
233 wh.update(e.input);
200234
201 wh = Wyhash.init(6);
202 var i: u32 = 0;
203 while (i < count) : (i += 1) {
204 wh.update(pattern);
235 for (0..10) |_| {
236 try expectEqual(e.expected, wh.final());
205237 }
206 try expectEqual(wh.final(), result);
207238}
208239
209240test "iterative non-divisible update" {