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

Fixed hashcode for StackItem #3531

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions src/Neo.VM/JumpTable/JumpTable.Compound.cs
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,7 @@ public virtual void SetItem(ExecutionEngine engine, Instruction instruction)
if (b < sbyte.MinValue || b > byte.MaxValue)
throw new InvalidOperationException($"Overflow in {instruction.OpCode}, {b} is not a byte type.");
buffer.InnerBuffer.Span[index] = (byte)b;
buffer.Invalidate();
break;
}
default:
Expand All @@ -489,6 +490,7 @@ public virtual void ReverseItems(ExecutionEngine engine, Instruction instruction
break;
case Types.Buffer buffer:
buffer.InnerBuffer.Span.Reverse();
buffer.Invalidate();
break;
default:
throw new InvalidOperationException($"Invalid type for {instruction.OpCode}: {x.Type}");
Expand Down
1 change: 1 addition & 0 deletions src/Neo.VM/JumpTable/JumpTable.Splice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public virtual void Memcpy(ExecutionEngine engine, Instruction instruction)
throw new InvalidOperationException($"The value {count} is out of range.");
// TODO: check if we can optimize the memcpy by using peek instead of dup then pop
src.Slice(si, count).CopyTo(dst.InnerBuffer.Span[di..]);
dst.Invalidate();
}

/// <summary>
Expand Down
11 changes: 11 additions & 0 deletions src/Neo.VM/Script.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ namespace Neo.VM
[DebuggerDisplay("Length={Length}")]
public class Script
{
private int _hashCode = 0;
private readonly ReadOnlyMemory<byte> _value;
private readonly bool strictMode;
private readonly Dictionary<int, Instruction> _instructions = new();
Expand Down Expand Up @@ -157,5 +158,15 @@ public Instruction GetInstruction(int ip)
public static implicit operator ReadOnlyMemory<byte>(Script script) => script._value;
public static implicit operator Script(ReadOnlyMemory<byte> script) => new(script);
public static implicit operator Script(byte[] script) => new(script);

public override int GetHashCode()
{
if (_hashCode == 0)
{
return _hashCode = (int)Unsafe.HashBytes(_value.Span);
}

return _hashCode;
}
}
}
10 changes: 10 additions & 0 deletions src/Neo.VM/Types/Buffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Numerics;
using System.Runtime.CompilerServices;

namespace Neo.VM.Types
{
Expand Down Expand Up @@ -112,5 +113,14 @@ public override string ToString()
{
return GetSpan().TryGetString(out var str) ? $"(\"{str}\")" : $"(\"Base64: {Convert.ToBase64String(GetSpan())}\")";
}

/// <summary>
/// Invalidate
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Invalidate()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a good practice?
Then we will need a specific check for checking is it is 0.
Why not null for example?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shargon added, but it int cant be null, plus its cached

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invalidate reset the hash method, is only in buffer items

{
_hashCode = 0;
}
}
}
15 changes: 0 additions & 15 deletions src/Neo.VM/Types/ByteString.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@
// Redistribution and use in source and binary forms with or without
// modifications are permitted.

using Neo.VM.Cryptography;
using System;
using System.Buffers.Binary;
using System.Diagnostics;
using System.Numerics;
using System.Runtime.CompilerServices;
Expand All @@ -29,9 +27,6 @@ public class ByteString : PrimitiveType
/// </summary>
public static readonly ByteString Empty = ReadOnlyMemory<byte>.Empty;

private static readonly uint s_seed = unchecked((uint)new Random().Next());
private int _hashCode = 0;

public override ReadOnlyMemory<byte> Memory { get; }
public override StackItemType Type => StackItemType.ByteString;

Expand Down Expand Up @@ -88,16 +83,6 @@ public override bool GetBoolean()
return Unsafe.NotZero(GetSpan());
}

public override int GetHashCode()
{
if (_hashCode == 0)
{
using Murmur32 murmur = new(s_seed);
_hashCode = BinaryPrimitives.ReadInt32LittleEndian(murmur.ComputeHash(GetSpan().ToArray()));
}
return _hashCode;
}

public override BigInteger GetInteger()
{
if (Size > Integer.MaxSize) throw new InvalidCastException($"MaxSize exceed: {Size}");
Expand Down
2 changes: 1 addition & 1 deletion src/Neo.VM/Types/Pointer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public override bool GetBoolean()

public override int GetHashCode()
{
return HashCode.Combine(Script, Position);
return HashCode.Combine(Script.GetHashCode(), Position);
}

public override string ToString()
Expand Down
6 changes: 0 additions & 6 deletions src/Neo.VM/Types/PrimitiveType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,6 @@ internal sealed override StackItem DeepCopy(Dictionary<StackItem, StackItem> ref

public abstract override bool Equals(StackItem? other);

/// <summary>
/// Get the hash code of the VM object, which is used for key comparison in the <see cref="Map"/>.
/// </summary>
/// <returns>The hash code of this VM object.</returns>
public abstract override int GetHashCode();

public sealed override ReadOnlySpan<byte> GetSpan()
{
return Memory.Span;
Expand Down
18 changes: 15 additions & 3 deletions src/Neo.VM/Types/StackItem.Vertex.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
// Redistribution and use in source and binary forms with or without
// modifications are permitted.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;

namespace Neo.VM.Types
{
Expand Down Expand Up @@ -80,6 +80,11 @@ internal class ObjectReferenceEntry
/// </summary>
internal int LowLink = 0;

/// <summary>
/// Stack Item hashcode
/// </summary>
protected int _hashCode = 0;

/// <summary>
/// Indicates whether the item is currently on the stack for Tarjan's algorithm.
///
Expand Down Expand Up @@ -120,7 +125,14 @@ internal class ObjectReferenceEntry
/// Use this method when you need a hash code for a StackItem.
/// </summary>
/// <returns>The hash code for the StackItem.</returns>
public override int GetHashCode() =>
HashCode.Combine(GetSpan().ToArray());
public override int GetHashCode()
{
if (_hashCode == 0)
{
return _hashCode = (int)Unsafe.HashBytes(GetSpan());
}

return _hashCode;
shargon marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
37 changes: 37 additions & 0 deletions src/Neo.VM/Unsafe.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ namespace Neo.VM
{
unsafe internal static class Unsafe
{
const long HashMagicNumber = 40343;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool NotZero(ReadOnlySpan<byte> x)
{
Expand All @@ -38,5 +40,40 @@ public static bool NotZero(ReadOnlySpan<byte> x)
}
return false;
}

/// <summary>
/// Get 64-bit hash code for a byte array
/// </summary>
/// <param name="span">Span</param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static long HashBytes(ReadOnlySpan<byte> span)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return int and avoid casts?

Copy link
Member Author

@cschuchardt88 cschuchardt88 Oct 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's an 64-bit hash function. However will be safe because of the VM limits, but anything bigger than 2GB it will overflow.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any other int32 hash method?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here a simple one. Just remember the one I in code atm is faster

public override int GetHashCode()
{
    if (byteArray == null || byteArray.Length == 0)
        return 0;

    unchecked // Overflow is fine, just wrap
    {
        int hash = 17; // Seed with a prime number
        foreach (byte b in byteArray)
        {
            hash = hash * 31 + b; // Multiply by a prime number and add byte value
        }
        return hash;
    }
}

{
var len = span.Length;
var hashState = (ulong)len;

fixed (byte* k = span)
{
var pwString = (char*)k;
var cbBuf = len / 2;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cschuchardt88 it doesn't iterate all the span? is that expected @cschuchardt88 ?

Copy link
Member Author

@cschuchardt88 cschuchardt88 Oct 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its using char* witch is 2-bytes.


for (var i = 0; i < cbBuf; i++, pwString++)
hashState = HashMagicNumber * hashState + *pwString;

if ((len & 1) > 0)
{
var pC = (byte*)pwString;
hashState = HashMagicNumber * hashState + *pC;
}
}

return (long)Rotr64(HashMagicNumber * hashState, 4);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static ulong Rotr64(ulong x, int n)
{
return ((x) >> n) | ((x) << (64 - n));
}
}
}
11 changes: 10 additions & 1 deletion tests/Neo.VM.Tests/UT_StackItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,17 @@ public void TestHashCode()

itemA = new VM.Types.Buffer(1);
itemB = new VM.Types.Buffer(1);
itemC = new VM.Types.Buffer(2);

Assert.IsTrue(itemA.GetHashCode() != itemB.GetHashCode());
Assert.IsTrue(itemA.GetHashCode() == itemB.GetHashCode());
Assert.IsTrue(itemA.GetHashCode() != itemC.GetHashCode());

itemA = new byte[] { 1, 2, 3 };
itemB = new byte[] { 1, 2, 3 };
itemC = new byte[] { 5, 6 };

Assert.IsTrue(itemA.GetHashCode() == itemB.GetHashCode());
Assert.IsTrue(itemA.GetHashCode() != itemC.GetHashCode());

itemA = true;
itemB = true;
Expand Down
Loading