Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Optimization]: Make murmur32 appendable and faster. #3760

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
111 changes: 80 additions & 31 deletions src/Neo/Cryptography/Murmur32.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

using System;
using System.Buffers.Binary;
using System.IO.Hashing;
using System.Runtime.CompilerServices;

namespace Neo.Cryptography
Expand All @@ -19,7 +20,7 @@ namespace Neo.Cryptography
/// Computes the murmur hash for the input data.
/// <remarks>Murmur32 is a non-cryptographic hash function.</remarks>
/// </summary>
public sealed class Murmur32
public sealed class Murmur32 : NonCryptographicHashAlgorithm
{
private const uint c1 = 0xcc9e2d51;
private const uint c2 = 0x1b873593;
Expand All @@ -32,6 +33,9 @@ public sealed class Murmur32
private uint _hash;
private int _length;

private uint _tail;
private int _tailLength;

public const int HashSizeInBits = 32;

[Obsolete("Use HashSizeInBits")]
Expand All @@ -41,46 +45,74 @@ public sealed class Murmur32
/// Initializes a new instance of the <see cref="Murmur32"/> class with the specified seed.
/// </summary>
/// <param name="seed">The seed to be used.</param>
public Murmur32(uint seed)
public Murmur32(uint seed) : base(HashSizeInBits / 8)
{
_seed = seed;
Reset();
}

/// <summary>
/// Append data to murmur computation
/// </summary>
/// <param name="source">Source</param>
private void Append(ReadOnlySpan<byte> source)
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void Reset()
{
_hash = _seed;
_length = 0;
_tailLength = 0;
_tail = 0;
}

/// <inheritdoc/>
public override void Append(ReadOnlySpan<byte> source)
{
_length += source.Length;
if (_tailLength > 0)
{
int remaining = Math.Min(4 - _tailLength, source.Length);
_tail ^= (ReadUInt32(source[..remaining]) << (_tailLength * 8));
_tailLength += remaining;
if (_tailLength == 4)
{
Mix(_tail);
_tailLength = 0;
_tail = 0;
}
source = source[remaining..];
}

#if NET7_0_OR_GREATER
shargon marked this conversation as resolved.
Show resolved Hide resolved
for (; source.Length >= 16; source = source[16..])
{
var k = BinaryPrimitives.ReadUInt128LittleEndian(source);
Mix((uint)k);
Mix((uint)(k >> 32));
Mix((uint)(k >> 64));
Mix((uint)(k >> 96));
}
#endif

for (; source.Length >= 4; source = source[4..])
{
var k = BinaryPrimitives.ReadUInt32LittleEndian(source);
k *= c1;
k = Helper.RotateLeft(k, r1);
k *= c2;
_hash ^= k;
_hash = Helper.RotateLeft(_hash, r2);
_hash = _hash * m + n;
Mix(BinaryPrimitives.ReadUInt32LittleEndian(source));
}

if (source.Length > 0)
{
uint remainingBytes = 0;
switch (source.Length)
{
case 3: remainingBytes ^= (uint)source[2] << 16; goto case 2;
case 2: remainingBytes ^= (uint)source[1] << 8; goto case 1;
case 1: remainingBytes ^= source[0]; break;
}
remainingBytes *= c1;
remainingBytes = Helper.RotateLeft(remainingBytes, r1);
remainingBytes *= c2;
_hash ^= remainingBytes;
_tail = ReadUInt32(source);
_tailLength = source.Length;
}
}

private uint GetCurrentHashUInt32()
/// <inheritdoc/>
protected override void GetCurrentHashCore(Span<byte> destination)
{
BinaryPrimitives.WriteUInt32LittleEndian(destination, GetCurrentHashUInt32());
}

internal uint GetCurrentHashUInt32()
{
if (_tailLength > 0)
_hash ^= Helper.RotateLeft(_tail * c1, r1) * c2;

var state = _hash ^ (uint)_length;
state ^= state >> 16;
state *= 0x85ebca6b;
Expand All @@ -91,10 +123,27 @@ private uint GetCurrentHashUInt32()
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Initialize()
private void Mix(uint k)
{
_hash = _seed;
_length = 0;
k *= c1;
k = Helper.RotateLeft(k, r1);
k *= c2;
_hash ^= k;
_hash = Helper.RotateLeft(_hash, r2);
_hash = _hash * m + n;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private uint ReadUInt32(ReadOnlySpan<byte> source)
{
uint value = 0;
switch (source.Length)
{
case 3: value ^= (uint)source[2] << 16; goto case 2;
case 2: value ^= (uint)source[1] << 8; goto case 1;
case 1: value ^= source[0]; break;
}
return value;
}

/// <summary>
Expand All @@ -110,14 +159,14 @@ public byte[] ComputeHash(ReadOnlySpan<byte> data)
}

/// <summary>
/// Computes the murmur hash for the input data and resets the state.
/// Resets the state and computes the murmur hash for the input data.
/// </summary>
/// <param name="data">The input to compute the hash code for.</param>
/// <returns>The computed hash code in uint.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public uint ComputeHashUInt32(ReadOnlySpan<byte> data)
{
Initialize();
Reset();
cschuchardt88 marked this conversation as resolved.
Show resolved Hide resolved
Append(data);
return GetCurrentHashUInt32();
}
Expand Down
46 changes: 46 additions & 0 deletions tests/Neo.UnitTests/Cryptography/UT_Murmur32.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

using Microsoft.VisualStudio.TestTools.UnitTesting;
using Neo.Cryptography;
using System;
using System.Buffers.Binary;

namespace Neo.UnitTests.Cryptography
Expand Down Expand Up @@ -47,6 +48,51 @@ public void TestComputeHashUInt32()
var murmur3 = new Murmur32(10u);
var hash = murmur3.ComputeHashUInt32("hello worldhello world"u8.ToArray());
Assert.AreEqual(60539726u, hash);

hash = murmur3.ComputeHashUInt32("he"u8.ToArray());
Assert.AreEqual(972873329u, hash);
}

[TestMethod]
public void TestAppend()
{
var murmur3 = new Murmur32(10u);
murmur3.Append("h"u8.ToArray());
murmur3.Append("e"u8.ToArray());
Assert.AreEqual(972873329u, murmur3.GetCurrentHashUInt32());

murmur3.Reset();
murmur3.Append("hello world"u8.ToArray());
murmur3.Append("hello world"u8.ToArray());
Assert.AreEqual(60539726u, murmur3.GetCurrentHashUInt32());

murmur3.Reset();
murmur3.Append("hello worldh"u8.ToArray());
murmur3.Append("ello world"u8.ToArray());
Assert.AreEqual(60539726u, murmur3.GetCurrentHashUInt32());

murmur3.Reset();
murmur3.Append("hello worldhello world"u8.ToArray());
murmur3.Append(""u8.ToArray());
Assert.AreEqual(60539726u, murmur3.GetCurrentHashUInt32());

murmur3.Reset();
murmur3.Append(""u8.ToArray());
murmur3.Append("hello worldhello world"u8.ToArray());
Assert.AreEqual(60539726u, murmur3.GetCurrentHashUInt32());

// random data, random split
var random = new Random();
var data = new byte[random.Next(1, 2048)];
random.NextBytes(data);
for (int i = 0; i < 32; i++)
{
var split = random.Next(1, data.Length - 1);
murmur3.Reset();
murmur3.Append(data.AsSpan(0, split));
murmur3.Append(data.AsSpan(split));
Assert.AreEqual(data.Murmur32(10u), murmur3.GetCurrentHashUInt32());
}
}
}
}
Loading