Skip to content

Commit 04eac2c

Browse files
committed
add XXHash.Hash64Async()
1 parent f5a11c8 commit 04eac2c

3 files changed

Lines changed: 127 additions & 40 deletions

File tree

src/XXHash.State64.cs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// <copyright file="XXHash.State64.cs" company="Sedat Kapanoglu">
2+
// Copyright (c) 2015-2025 Sedat Kapanoglu
3+
// MIT License (see LICENSE file for details)
4+
// </copyright>
5+
6+
using System;
7+
8+
namespace HashDepot;
9+
10+
/// <summary>
11+
/// State machine for XXHash 32-bit algorithm's streaming functions.
12+
/// </summary>
13+
public static partial class XXHash
14+
{
15+
class State64
16+
{
17+
public const int StripeLength = 32;
18+
readonly ulong seed;
19+
ulong acc;
20+
ulong acc1;
21+
ulong acc2;
22+
ulong acc3;
23+
ulong acc4;
24+
int len;
25+
bool finalBlockProcessed;
26+
27+
public State64(ulong seed)
28+
{
29+
this.seed = seed;
30+
(acc1, acc2, acc3, acc4) = initAccumulators64(seed);
31+
}
32+
33+
public void Update(ReadOnlySpan<byte> buffer)
34+
{
35+
if (finalBlockProcessed)
36+
{
37+
throw new InvalidOperationException("Update() called after final stripe has been processed.");
38+
}
39+
40+
int bufferSize = buffer.Length;
41+
42+
// first call?
43+
if (len == 0 && bufferSize < StripeLength)
44+
{
45+
acc = seed + prime64v5;
46+
len += bufferSize;
47+
acc += (uint)len;
48+
acc = processRemaining64(buffer, acc);
49+
finalBlockProcessed = true;
50+
return;
51+
}
52+
53+
int offset = 0;
54+
len += bufferSize;
55+
56+
for (; bufferSize >= StripeLength; bufferSize -= StripeLength)
57+
{
58+
int end = offset + StripeLength;
59+
acc = processStripe64(
60+
buffer[offset..end],
61+
ref acc1,
62+
ref acc2,
63+
ref acc3,
64+
ref acc4);
65+
offset = end;
66+
}
67+
68+
// process the final incomplete stripe
69+
if (bufferSize > 0)
70+
{
71+
acc += (uint)len;
72+
acc = processRemaining64(buffer[offset..(offset + bufferSize)], acc);
73+
finalBlockProcessed = true;
74+
}
75+
}
76+
77+
public ulong Result()
78+
{
79+
if (len == 0)
80+
{
81+
acc = seed + prime64v5;
82+
}
83+
84+
if (!finalBlockProcessed)
85+
{
86+
acc += (uint)len;
87+
finalBlockProcessed = true;
88+
}
89+
90+
return avalanche64(acc);
91+
}
92+
}
93+
}

src/XXHash.cs

Lines changed: 24 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -149,54 +149,38 @@ public static ulong Hash64(ReadOnlySpan<byte> buffer, ulong seed = 0)
149149
/// <returns>Computed 64-bit hash value.</returns>
150150
public static ulong Hash64(Stream stream, ulong seed = 0)
151151
{
152-
const int stripeLength = 32;
153-
const int readBufferSize = stripeLength * 1024; // 32kb buffer length
154-
155-
ulong acc;
152+
const int readBufferSize = State64.StripeLength * 1024; // 32kb buffer length
156153

157154
var buffer = new byte[readBufferSize].AsSpan();
158-
int readBytes = stream.Read(buffer);
159-
ulong len = (ulong)readBytes;
160-
161-
int offset = 0;
162-
if (readBytes < stripeLength)
155+
var state = new State64(seed);
156+
int bytesRead;
157+
while ((bytesRead = stream.Read(buffer)) > 0)
163158
{
164-
acc = seed + prime64v5;
165-
goto Exit;
159+
state.Update(buffer[..bytesRead]);
166160
}
167161

168-
var (acc1, acc2, acc3, acc4) = initAccumulators64(seed);
169-
do
162+
return state.Result();
163+
}
164+
165+
/// <summary>
166+
/// Generate a 64-bit xxHash value from a stream.
167+
/// </summary>
168+
/// <param name="stream">Input stream.</param>
169+
/// <param name="seed">Optional seed.</param>
170+
/// <returns>Computed 64-bit hash value.</returns>
171+
public static async Task<ulong> Hash64Async(Stream stream, ulong seed = 0)
172+
{
173+
const int readBufferSize = State64.StripeLength * 1024; // 32kb buffer length
174+
175+
var buffer = new byte[readBufferSize];
176+
var state = new State64(seed);
177+
int bytesRead;
178+
while ((bytesRead = await stream.ReadAsync(buffer)) > 0)
170179
{
171-
do
172-
{
173-
int end = offset + stripeLength;
174-
acc = processStripe64(
175-
buffer[offset..end],
176-
ref acc1,
177-
ref acc2,
178-
ref acc3,
179-
ref acc4);
180-
offset = end;
181-
readBytes -= stripeLength;
182-
}
183-
while (readBytes >= stripeLength);
184-
185-
// read more if the alignment is intact
186-
if (readBytes == 0)
187-
{
188-
readBytes = stream.Read(buffer);
189-
offset = 0;
190-
len += (ulong)readBytes;
191-
}
180+
state.Update(buffer.AsSpan(0, bytesRead));
192181
}
193-
while (readBytes >= stripeLength);
194182

195-
Exit:
196-
acc += len;
197-
acc = processRemaining64(buffer[offset..(offset + readBytes)], acc);
198-
199-
return avalanche64(acc);
183+
return state.Result();
200184
}
201185

202186
[MethodImpl(MethodImplOptions.AggressiveInlining)]

test/XXHashTest.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,16 @@ public void Hash64_StreamTests(string text, uint seed, uint _, ulong hash64)
7272
Assert.That(result, Is.EqualTo(hash64));
7373
}
7474

75+
[Test]
76+
[TestCaseSource(nameof(testVectors))]
77+
public async Task Hash64Async_StreamTests(string text, uint seed, uint _, ulong hash64)
78+
{
79+
var buffer = Encoding.UTF8.GetBytes(text);
80+
using var stream = new MemoryStream(buffer);
81+
var result = await XXHash.Hash64Async(stream, seed);
82+
Assert.That(result, Is.EqualTo(hash64));
83+
}
84+
7585
[Test]
7686
public void Hash32_LongStream_ReturnsExpected()
7787
{

0 commit comments

Comments
 (0)