Skip to content

Commit

Permalink
Merge pull request #4668 from mithileshz/StreamsComparer
Browse files Browse the repository at this point in the history
Use MemoryExtensions in StreamsComparer for performance
  • Loading branch information
stevenaw committed Mar 23, 2024
2 parents 2f9d052 + 8a0a34f commit 328dc52
Show file tree
Hide file tree
Showing 5 changed files with 261 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/NUnitFramework/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<!-- Packages for used features -->
<ItemGroup>
<PackageVersion Include="System.Collections.Immutable" Version="6.0.0" />
<PackageVersion Include="System.Memory" Version="4.5.4" />
<PackageVersion Include="System.ValueTuple" version="4.5.0" />
</ItemGroup>
<!-- General Packages -->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt

using BenchmarkDotNet.Attributes;

namespace NUnit.Framework
{
public class StreamsComparerBenchmark
{
private const int BUFFER_SIZE = 4096;

[Params(4096 * 8)]
public int Size { get; set; }
private Stream? XStream { get; set; }
private Stream? YStream { get; set; }

[GlobalSetup]
public void Setup()
{
var buffer = new byte[BUFFER_SIZE];

XStream = new MemoryStream(Size);
for (var i = 0; i < Size; i += BUFFER_SIZE)
XStream.Write(buffer, 0, BUFFER_SIZE);
XStream.Seek(0, SeekOrigin.Begin);

YStream = new MemoryStream(Size);
for (var i = 0; i < Size; i += BUFFER_SIZE)
YStream.Write(buffer, 0, BUFFER_SIZE);
YStream.Seek(0, SeekOrigin.Begin);
}

[GlobalCleanup]
public void Cleanup()
{
XStream?.Dispose();

YStream?.Dispose();
}

[Benchmark(Baseline = true)]
public bool Original()
{
var equal = Equal_Original(XStream!, YStream!, out _);
return equal == EqualMethodResult.ComparedEqual;
}

[Benchmark]
public bool Vectorized()
{
var equal = Equal_Enhanced(XStream!, YStream!, out _);
return equal == EqualMethodResult.ComparedEqual;
}

public static EqualMethodResult Equal_Original(Stream xStream, Stream yStream, out long? failurePoint)
{
failurePoint = null;
bool bothSeekable = xStream.CanSeek && yStream.CanSeek;

if (bothSeekable && xStream.Length != yStream.Length)
return EqualMethodResult.ComparedNotEqual;

byte[] bufferExpected = new byte[BUFFER_SIZE];
byte[] bufferActual = new byte[BUFFER_SIZE];

BinaryReader binaryReaderExpected = new BinaryReader(xStream);
BinaryReader binaryReaderActual = new BinaryReader(yStream);

long expectedPosition = bothSeekable ? xStream.Position : default;
long actualPosition = bothSeekable ? yStream.Position : default;

try
{
if (xStream.CanSeek)
{
binaryReaderExpected.BaseStream.Seek(0, SeekOrigin.Begin);
}
if (yStream.CanSeek)
{
binaryReaderActual.BaseStream.Seek(0, SeekOrigin.Begin);
}

int readExpected = 1;
int readActual = 1;
long readByte = 0;

while (readExpected > 0 && readActual > 0)
{
readExpected = binaryReaderExpected.Read(bufferExpected, 0, BUFFER_SIZE);
readActual = binaryReaderActual.Read(bufferActual, 0, BUFFER_SIZE);

for (int count = 0; count < BUFFER_SIZE; ++count)
{
if (bufferExpected[count] != bufferActual[count])
{
failurePoint = readByte + count;
return EqualMethodResult.ComparedNotEqual;
}
}
readByte += BUFFER_SIZE;
}
}
finally
{
if (xStream.CanSeek)
{
xStream.Position = expectedPosition;
}
if (yStream.CanSeek)
{
yStream.Position = actualPosition;
}
}

return EqualMethodResult.ComparedEqual;
}

public static EqualMethodResult Equal_Enhanced(Stream xStream, Stream yStream, out long? failurePoint)
{
failurePoint = null;
bool bothSeekable = xStream.CanSeek && yStream.CanSeek;

if (bothSeekable && xStream.Length != yStream.Length)
return EqualMethodResult.ComparedNotEqual;

byte[] bufferExpected = new byte[BUFFER_SIZE];
byte[] bufferActual = new byte[BUFFER_SIZE];

BinaryReader binaryReaderExpected = new BinaryReader(xStream);
BinaryReader binaryReaderActual = new BinaryReader(yStream);

long expectedPosition = bothSeekable ? xStream.Position : default;
long actualPosition = bothSeekable ? yStream.Position : default;

try
{
if (xStream.CanSeek)
{
binaryReaderExpected.BaseStream.Seek(0, SeekOrigin.Begin);
}
if (yStream.CanSeek)
{
binaryReaderActual.BaseStream.Seek(0, SeekOrigin.Begin);
}

int readExpected = 1;
int readActual = 1;
long readByte = 0;

while (readExpected > 0 && readActual > 0)
{
readExpected = binaryReaderExpected.Read(bufferExpected, 0, BUFFER_SIZE);
readActual = binaryReaderActual.Read(bufferActual, 0, BUFFER_SIZE);

if (MemoryExtensions.SequenceEqual<byte>(bufferExpected, bufferActual))
{
readByte += BUFFER_SIZE;
continue;
}

for (int count = 0; count < BUFFER_SIZE; ++count)
{
if (bufferExpected[count] != bufferActual[count])
{
failurePoint = readByte + count;
return EqualMethodResult.ComparedNotEqual;
}
}
readByte += BUFFER_SIZE;
}
}
finally
{
if (xStream.CanSeek)
{
xStream.Position = expectedPosition;
}
if (yStream.CanSeek)
{
yStream.Position = actualPosition;
}
}

return EqualMethodResult.ComparedEqual;
}

/// <summary>
/// Result of the Equal comparison method.
/// </summary>
public enum EqualMethodResult
{
/// <summary>
/// Method does not support the instances being compared.
/// </summary>
TypesNotSupported,

/// <summary>
/// Method is appropriate for the data types, but doesn't support the specified tolerance.
/// </summary>
ToleranceNotSupported,

/// <summary>
/// Method is appropriate and the items are considered equal.
/// </summary>
ComparedEqual,

/// <summary>
/// Method is appropriate and the items are considered different.
/// </summary>
ComparedNotEqual,

/// <summary>
/// Method is appropriate but the class has cyclic references.
/// </summary>
ComparisonPending
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ public static EqualMethodResult Equal(object x, object y, ref Tolerance toleranc
readExpected = binaryReaderExpected.Read(bufferExpected, 0, BUFFER_SIZE);
readActual = binaryReaderActual.Read(bufferActual, 0, BUFFER_SIZE);

if (MemoryExtensions.SequenceEqual<byte>(bufferExpected, bufferActual))
{
readByte += BUFFER_SIZE;
continue;
}

for (int count = 0; count < BUFFER_SIZE; ++count)
{
if (bufferExpected[count] != bufferActual[count])
Expand Down
4 changes: 4 additions & 0 deletions src/NUnitFramework/framework/nunit.framework.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
<Compile Include="..\FrameworkVersion.cs" Link="Properties\FrameworkVersion.cs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="System.Memory" />
</ItemGroup>

<ItemGroup>
<None Update="Schemas\*.xsd" CopyToOutputDirectory="Always" />
</ItemGroup>
Expand Down
33 changes: 33 additions & 0 deletions src/NUnitFramework/tests/Constraints/EqualConstraintTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Globalization;
using System.IO.Compression;
using System.IO;
using System.Linq;
using System.Text;

using NUnit.Framework.Constraints;
Expand Down Expand Up @@ -157,6 +158,38 @@ public void UnSeekableActualAndExpectedStreamsUnequal()
Assert.That(expectedStream, Is.Not.EqualTo(actualStream));
}

[Test]
public void UnSeekableLargeActualStreamEqual()
{
// This creates a string that exceeds 4096 bytes for the StreamsComparer loop.
string streamValue = string.Concat(Enumerable.Repeat("Greetings from a stream that is from the other side!", 100));

using var expectedStream = new MemoryStream(Encoding.UTF8.GetBytes(streamValue));

using var actualArchive = CreateZipArchive(streamValue);
ZipArchiveEntry entry = actualArchive.Entries[0];

using Stream entryStream = entry.Open();
Assert.That(entryStream, Is.EqualTo(expectedStream));
}

[Test]
public void UnSeekableLargeActualStreamUnequal()
{
// This creates a string that exceeds 4096 bytes for the StreamsComparer loop.
string streamValue = string.Concat(Enumerable.Repeat("Greetings from a stream that is from the other side!", 100));

string unequalStream = string.Concat(streamValue, "Some extra difference at the end.");

using var expectedStream = new MemoryStream(Encoding.UTF8.GetBytes(streamValue));

using var actualArchive = CreateZipArchive(unequalStream);
ZipArchiveEntry entry = actualArchive.Entries[0];

using Stream entryStream = entry.Open();
Assert.That(entryStream, Is.Not.EqualTo(expectedStream));
}

private static ZipArchive CreateZipArchive(string content)
{
var archiveContents = new MemoryStream();
Expand Down

0 comments on commit 328dc52

Please sign in to comment.