Skip to content

Commit

Permalink
Improve Utils.IsEqual for byte compare (OPCFoundation#2440)
Browse files Browse the repository at this point in the history
- Improve the `Utils.IsEqual` helper for some special cases which compare byte[].
- In the actual implementation the compilers can not optimize for special cases, e.g. comparing a typical certificate as byte[] with 1k size on .NET8 took 167µs, the customized version needs only 69ns and is even faster than a P/Invoke to memc mp (80ns). 
- see benchmark result for improvements
- Due to the little difference between a custom byte[] and the T[] compare, the byte[] was moved back to the tests
- .NET Framework falls back on memcmp
  • Loading branch information
mregen authored Dec 22, 2023
1 parent cd893a6 commit af7bb8c
Show file tree
Hide file tree
Showing 5 changed files with 674 additions and 20 deletions.
113 changes: 104 additions & 9 deletions Stack/Opc.Ua.Core/Types/Utils/Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1874,6 +1874,95 @@ public static bool IsEqual(DateTime time1, DateTime time2)
return utcTime1.CompareTo(utcTime2) == 0;
}

/// <summary>
/// Checks if two T values are equal based on IEquatable compare.
/// </summary>
public static bool IsEqual<T>(T value1, T value2) where T : IEquatable<T>
{
// check for reference equality.
if (Object.ReferenceEquals(value1, value2))
{
return true;
}

if (Object.ReferenceEquals(value1, null))
{
if (!Object.ReferenceEquals(value2, null))
{
return value2.Equals(value1);
}

return true;
}

// use IEquatable comparer
return value1.Equals(value2);
}

/// <summary>
/// Checks if two IEnumerable T values are equal.
/// </summary>
public static bool IsEqual<T>(IEnumerable<T> value1, IEnumerable<T> value2) where T : IEquatable<T>
{
// check for reference equality.
if (Object.ReferenceEquals(value1, value2))
{
return true;
}

if (Object.ReferenceEquals(value1, null) || Object.ReferenceEquals(value2, null))
{
return false;
}

return value1.SequenceEqual(value2);
}

/// <summary>
/// Checks if two T[] values are equal.
/// </summary>
public static bool IsEqual<T>(T[] value1, T[] value2) where T : unmanaged, IEquatable<T>
{
// check for reference equality.
if (Object.ReferenceEquals(value1, value2))
{
return true;
}

if (Object.ReferenceEquals(value1, null) || Object.ReferenceEquals(value2, null))
{
return false;
}

return value1.SequenceEqual(value2);
}

#if NETFRAMEWORK
[DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern int memcmp(byte[] b1, byte[] b2, long count);

/// <summary>
/// Fast memcpy version of byte[] compare.
/// </summary>
public static bool IsEqual(byte[] value1, byte[] value2)
{
// check for reference equality.
if (Object.ReferenceEquals(value1, value2))
{
return true;
}

if (Object.ReferenceEquals(value1, null) || Object.ReferenceEquals(value2, null))
{
return false;
}

// Validate buffers are the same length.
// This also ensures that the count does not exceed the length of either buffer.
return value1.Length == value2.Length && memcmp(value1, value2, value1.Length) == 0;
}
#endif

/// <summary>
/// Checks if two values are equal.
/// </summary>
Expand All @@ -1886,9 +1975,9 @@ public static bool IsEqual(object value1, object value2)
}

// check for null values.
if (value1 == null)
if (Object.ReferenceEquals(value1, null))
{
if (value2 != null)
if (!Object.ReferenceEquals(value2, null))
{
return value2.Equals(value1);
}
Expand All @@ -1897,12 +1986,12 @@ public static bool IsEqual(object value1, object value2)
}

// check for null values.
if (value2 == null)
if (Object.ReferenceEquals(value2, null))
{
return value1.Equals(value2);
}

// check that data types are the same.
// check that data types are not the same.
if (value1.GetType() != value2.GetType())
{
return value1.Equals(value2);
Expand All @@ -1915,14 +2004,12 @@ public static bool IsEqual(object value1, object value2)
}

// check for compareable objects.

if (value1 is IComparable comparable1)
{
return comparable1.CompareTo(value2) == 0;
}

// check for encodeable objects.

if (value1 is IEncodeable encodeable1)
{
if (!(value2 is IEncodeable encodeable2))
Expand All @@ -1934,7 +2021,6 @@ public static bool IsEqual(object value1, object value2)
}

// check for XmlElement objects.

if (value1 is XmlElement element1)
{
if (!(value2 is XmlElement element2))
Expand All @@ -1946,7 +2032,6 @@ public static bool IsEqual(object value1, object value2)
}

// check for arrays.

if (value1 is Array array1)
{
// arrays are greater than non-arrays.
Expand Down Expand Up @@ -1977,6 +2062,16 @@ public static bool IsEqual(object value1, object value2)
}
}

// handle byte[] special case fast
if (array1 is byte[] byteArray1 && array2 is byte[] byteArray2)
{
#if NETFRAMEWORK
return memcmp(byteArray1, byteArray2, byteArray1.Length) == 0;
#else
return byteArray1.SequenceEqual(byteArray2);
#endif
}

IEnumerator enumerator1 = array1.GetEnumerator();
IEnumerator enumerator2 = array2.GetEnumerator();

Expand Down Expand Up @@ -2434,7 +2529,7 @@ public static void UpdateExtension<T>(ref XmlElementCollection extensions, XmlQu
extensions.Add(document.DocumentElement);
}
}
#endregion
#endregion

#region Reflection Helper Functions
/// <summary>
Expand Down
1 change: 1 addition & 0 deletions Tests/Opc.Ua.Core.Tests/Opc.Ua.Core.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<TargetFrameworks>$(TestsTargetFrameworks)</TargetFrameworks>
<RootNamespace>Opc.Ua.Core.Tests</RootNamespace>
<GenerateProgramFile>false</GenerateProgramFile>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public class BinaryEncoderBenchmarks
/// </summary>
[Benchmark]
[Test]
public void BinaryEncoder_Constructor2()
public void BinaryEncoderConstructor2()
{
using (var binaryEncoder = new BinaryEncoder(m_context))
{
Expand All @@ -68,7 +68,7 @@ public void BinaryEncoder_Constructor2()
/// </summary>
[Benchmark]
[Test]
public void BinaryEncoder_Constructor3()
public void BinaryEncoderConstructor3()
{
using (var stream = new ArraySegmentStream(m_bufferManager, kBufferSize, 0, kBufferSize))
{
Expand All @@ -85,7 +85,7 @@ public void BinaryEncoder_Constructor3()
/// </summary>
[Benchmark]
[Test]
public void BinaryEncoder_Constructor_Streamwriter2()
public void BinaryEncoderConstructorStreamwriter2()
{
using (IEncoder binaryEncoder = new BinaryEncoder(m_memoryStream, m_context, true))
{
Expand All @@ -100,7 +100,7 @@ public void BinaryEncoder_Constructor_Streamwriter2()
/// </summary>
[Benchmark]
[Test]
public void BinaryEncoder_StreamLeaveOpen_MemoryStream()
public void BinaryEncoderStreamLeaveOpenMemoryStream()
{
BinaryEncoder_StreamLeaveOpen(m_memoryStream);
}
Expand All @@ -110,7 +110,7 @@ public void BinaryEncoder_StreamLeaveOpen_MemoryStream()
/// </summary>
[Benchmark]
[Test]
public void BinaryEncoder_StreamLeaveOpen_RecyclableMemoryStream()
public void BinaryEncoderStreamLeaveOpenRecyclableMemoryStream()
{
BinaryEncoder_StreamLeaveOpen(m_recyclableMemoryStream);
}
Expand All @@ -120,7 +120,7 @@ public void BinaryEncoder_StreamLeaveOpen_RecyclableMemoryStream()
/// </summary>
[Benchmark]
[Test]
public void BinaryEncoder_StreamLeaveOpen_ArraySegmentStream()
public void BinaryEncoderStreamLeaveOpenArraySegmentStream()
{
BinaryEncoder_StreamLeaveOpen(m_arraySegmentStream);
}
Expand All @@ -131,7 +131,7 @@ public void BinaryEncoder_StreamLeaveOpen_ArraySegmentStream()
/// </summary>
[Benchmark]
[Test]
public void BinaryEncoder_Constructor_Streamwriter_Reflection2()
public void BinaryEncoderConstructorStreamwriterReflection2()
{
using (var binaryEncoder = new BinaryEncoder(m_memoryStream, m_context, true))
{
Expand Down
Loading

0 comments on commit af7bb8c

Please sign in to comment.