Skip to content

Instantly share code, notes, and snippets.

@filzrev
Forked from neuecc/SuperFastFileComparer.cs
Created April 14, 2024 08:46
Show Gist options
  • Save filzrev/c6a020260ae742d5c66f89ef899574e0 to your computer and use it in GitHub Desktop.
Save filzrev/c6a020260ae742d5c66f89ef899574e0 to your computer and use it in GitHub Desktop.
public static class FileComparer
{
public static bool CompareEquals(string filePath1, string filePath2, int bufferSize = 65536)
{
if (filePath1 == filePath2) return true;
var buffer1 = ArrayPool<byte>.Shared.Rent(bufferSize);
var buffer2 = ArrayPool<byte>.Shared.Rent(bufferSize);
try
{
using var handle1 = File.OpenHandle(filePath1, FileMode.Open, FileAccess.Read, options: FileOptions.SequentialScan);
using var handle2 = File.OpenHandle(filePath2, FileMode.Open, FileAccess.Read, options: FileOptions.SequentialScan);
var length1 = RandomAccess.GetLength(handle1);
var length2 = RandomAccess.GetLength(handle2);
if (length1 == 0 && length2 == 0) return true;
if (length1 != length2) return false;
long fileOffset = 0;
while (fileOffset != length1)
{
var read1 = ReadFully(handle1, fileOffset, buffer1);
var read2 = ReadFully(handle2, fileOffset, buffer2);
if (read1 != read2) return false;
if (!buffer1.AsSpan(0, read1).SequenceEqual(buffer2.AsSpan(0, read2)))
{
return false;
}
fileOffset += read1;
}
return true;
}
finally
{
ArrayPool<byte>.Shared.Return(buffer1);
ArrayPool<byte>.Shared.Return(buffer2);
}
}
public static async ValueTask<bool> CompareEqualsAsync(string filePath1, string filePath2, int bufferSize = 65536, CancellationToken cancellationToken = default)
{
if (filePath1 == filePath2) return true;
var buffer1 = ArrayPool<byte>.Shared.Rent(bufferSize);
var buffer2 = ArrayPool<byte>.Shared.Rent(bufferSize);
try
{
using var handle1 = File.OpenHandle(filePath1, FileMode.Open, FileAccess.Read, options: FileOptions.SequentialScan | FileOptions.Asynchronous);
using var handle2 = File.OpenHandle(filePath2, FileMode.Open, FileAccess.Read, options: FileOptions.SequentialScan | FileOptions.Asynchronous);
var length1 = RandomAccess.GetLength(handle1);
var length2 = RandomAccess.GetLength(handle2);
if (length1 == 0 && length2 == 0) return true;
if (length1 != length2) return false;
long fileOffset = 0;
while (fileOffset != length1)
{
// inlined ReadFully
int read1 = 0;
{
var offset = fileOffset;
var buffer = buffer1.AsMemory();
var bufferLength = buffer1.Length;
while (read1 < bufferLength)
{
var read = await RandomAccess.ReadAsync(handle1, buffer, offset, cancellationToken).ConfigureAwait(false);
if (read == 0) break;
read1 += read;
offset += read;
buffer = buffer.Slice(read);
}
}
int read2 = 0;
{
var offset = fileOffset;
var buffer = buffer2.AsMemory();
var bufferLength = buffer2.Length;
while (read2 < bufferLength)
{
var read = await RandomAccess.ReadAsync(handle2, buffer, offset, cancellationToken).ConfigureAwait(false);
if (read == 0) break;
read2 += read;
offset += read;
buffer = buffer.Slice(read);
}
}
if (read1 != read2) return false;
if (!buffer1.AsSpan(0, read1).SequenceEqual(buffer2.AsSpan(0, read2)))
{
return false;
}
fileOffset += read1;
}
return true;
}
finally
{
ArrayPool<byte>.Shared.Return(buffer1);
ArrayPool<byte>.Shared.Return(buffer2);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static int ReadFully(SafeFileHandle handle, long fileOffset, Span<byte> buffer)
{
int bytesRead = 0;
var bufferLength = buffer.Length;
while (bytesRead < bufferLength)
{
var read = RandomAccess.Read(handle, buffer, fileOffset);
if (read == 0) return bytesRead;
bytesRead += read;
fileOffset += read;
buffer = buffer.Slice(read);
}
return bytesRead;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment