web-dev-qa-db-ja.com

.NETを使用して2つのファイルを高速に比較する方法は?

標準的なアプローチ FileStreamを介してバイナリを読み取り、バイト単位で比較することをお勧めします。

  • CRCなどのチェックサム比較は高速になりますか?
  • ファイルのチェックサムを生成できる.NETライブラリはありますか?
126
Robinicks

チェックサムの比較は、バイトごとの比較よりも遅い可能性があります。

チェックサムを生成するには、ファイルの各バイトをロードし、処理を実行する必要があります。次に、2番目のファイルでこれを行う必要があります。処理は、ほぼ間違いなく比較チェックよりも遅くなります。

チェックサムの生成に関して:暗号化クラスを使用すると、これを簡単に行うことができます。 MD5チェックサムを生成する簡単な例 C#を使用しています。

ただし、「テスト」または「ベース」ケースのチェックサムを事前に計算できる場合、チェックサムはより高速で、より意味があります。既存のファイルがあり、新しいファイルが既存のファイルと同じかどうかを確認する場合、「既存の」ファイルのチェックサムを事前計算することは、DiskIOを1回だけ実行することを意味します。新しいファイル。これは、バイト単位の比較よりも高速です。

109
Reed Copsey

最も遅い方法は、2つのファイルをバイト単位で比較することです。私が思いついた最速は同様の比較ですが、一度に1バイトではなく、Int64サイズのバイト配列を使用して、結果の数値を比較します。

ここに私が思いついたものがあります:

    const int BYTES_TO_READ = sizeof(Int64);

    static bool FilesAreEqual(FileInfo first, FileInfo second)
    {
        if (first.Length != second.Length)
            return false;

        if (string.Equals(first.FullName, second.FullName, StringComparison.OrdinalIgnoreCase))
            return true;

        int iterations = (int)Math.Ceiling((double)first.Length / BYTES_TO_READ);

        using (FileStream fs1 = first.OpenRead())
        using (FileStream fs2 = second.OpenRead())
        {
            byte[] one = new byte[BYTES_TO_READ];
            byte[] two = new byte[BYTES_TO_READ];

            for (int i = 0; i < iterations; i++)
            {
                 fs1.Read(one, 0, BYTES_TO_READ);
                 fs2.Read(two, 0, BYTES_TO_READ);

                if (BitConverter.ToInt64(one,0) != BitConverter.ToInt64(two,0))
                    return false;
            }
        }

        return true;
    }

私のテストでは、これはほぼ3:1の単純なReadByte()シナリオよりも優れていることがわかりました。平均して1000回の実行で、このメソッドは1063ミリ秒で取得し、以下のメソッド(バイトごとの直接比較)は3031ミリ秒で取得しました。ハッシュは常に平均約865msで1秒未満で戻ってきました。このテストでは、〜100MBのビデオファイルを使用しました。

以下に、比較のために使用したReadByteとハッシュメソッドを示します。

    static bool FilesAreEqual_OneByte(FileInfo first, FileInfo second)
    {
        if (first.Length != second.Length)
            return false;

        if (string.Equals(first.FullName, second.FullName, StringComparison.OrdinalIgnoreCase))
            return true;

        using (FileStream fs1 = first.OpenRead())
        using (FileStream fs2 = second.OpenRead())
        {
            for (int i = 0; i < first.Length; i++)
            {
                if (fs1.ReadByte() != fs2.ReadByte())
                    return false;
            }
        }

        return true;
    }

    static bool FilesAreEqual_Hash(FileInfo first, FileInfo second)
    {
        byte[] firstHash = MD5.Create().ComputeHash(first.OpenRead());
        byte[] secondHash = MD5.Create().ComputeHash(second.OpenRead());

        for (int i=0; i<firstHash.Length; i++)
        {
            if (firstHash[i] != secondHash[i])
                return false;
        }
        return true;
    }
122
chsh

Reed Copseyの答えに加えて:

  • 最悪のケースは、2つのファイルが同一の場合です。この場合、ファイルをバイト単位で比較するのが最善です。

  • 2つのファイルが同一でない場合、それらが同一でないことをより早く検出することにより、物事を少しスピードアップできます。

たとえば、2つのファイルの長さが異なる場合、それらを同一にすることはできず、実際の内容を比較する必要さえありません。

33
dtb

8バイトの小さなチャンクではなく、大きなチャンクをループでループすると、さらに高速になります。平均比較時間を1/4に短縮しました。

    public static bool FilesContentsAreEqual(FileInfo fileInfo1, FileInfo fileInfo2)
    {
        bool result;

        if (fileInfo1.Length != fileInfo2.Length)
        {
            result = false;
        }
        else
        {
            using (var file1 = fileInfo1.OpenRead())
            {
                using (var file2 = fileInfo2.OpenRead())
                {
                    result = StreamsContentsAreEqual(file1, file2);
                }
            }
        }

        return result;
    }

    private static bool StreamsContentsAreEqual(Stream stream1, Stream stream2)
    {
        const int bufferSize = 1024 * sizeof(Int64);
        var buffer1 = new byte[bufferSize];
        var buffer2 = new byte[bufferSize];

        while (true)
        {
            int count1 = stream1.Read(buffer1, 0, bufferSize);
            int count2 = stream2.Read(buffer2, 0, bufferSize);

            if (count1 != count2)
            {
                return false;
            }

            if (count1 == 0)
            {
                return true;
            }

            int iterations = (int)Math.Ceiling((double)count1 / sizeof(Int64));
            for (int i = 0; i < iterations; i++)
            {
                if (BitConverter.ToInt64(buffer1, i * sizeof(Int64)) != BitConverter.ToInt64(buffer2, i * sizeof(Int64)))
                {
                    return false;
                }
            }
        }
    }
}
15
Lars

チェックサム比較をバイト単位の比較よりもわずかに高速化できる唯一のことは、一度に1つのファイルを読み取っているという事実であり、ディスクヘッドのシーク時間を多少短縮します。しかし、そのわずかなゲインは、ハッシュの計算に追加される時間によって非常に食いつぶされる可能性があります。

また、もちろん、チェックサム比較は、ファイルが同一の場合にのみ高速になる可能性があります。そうでない場合、バイトごとの比較は最初の違いで終了し、はるかに高速になります。

また、ハッシュコードの比較では、可能性が高いがファイルが同一であることのみを通知することも考慮する必要があります。 100%確実にするには、バイトごとの比較を行う必要があります。

たとえば、ハッシュコードが32ビットの場合、ハッシュコードが一致すれば、ファイルが同一であることを約99.99999998%確信できます。それは100%に近いですが、本当に100%の確実性が必要な場合、それは違います。

15
Guffa

編集:この方法は、バイナリファイルを比較するためにnot動作します!

.NET 4.0では、Fileクラスに次の2つの新しいメソッドがあります。

public static IEnumerable<string> ReadLines(string path)
public static IEnumerable<string> ReadLines(string path, Encoding encoding)

それはあなたが使用できることを意味します:

bool same = File.ReadLines(path1).SequenceEqual(File.ReadLines(path2));
11
Sam Harwell

正直なところ、検索ツリーを可能な限り削除する必要があると思います。

バイトごとに進む前に確認すること:

  1. サイズは同じですか?
  2. ファイルAの最後のバイトはファイルBとは異なりますか

また、ドライブは連続したバイトをより速く読み取るため、一度に大きなブロックを読み取る方が効率的です。バイト単位で移動すると、システムコールがはるかに多くなるだけでなく、両方のファイルが同じドライブ上にある場合、従来のハードドライブの読み取りヘッドが頻繁に前後にシークします。

チャンクAとチャンクBをバイトバッファーに読み込み、比較します(Array.Equalsを使用しないでください、コメントを参照)。メモリとパフォーマンスのバランスが取れていると感じるまで、ブロックのサイズを調整します。比較をマルチスレッド化することもできますが、ディスク読み取りをマルチスレッド化しないでください。

6
RandomInsano

私の答えは@larsの派生物ですが、Stream.Readへの呼び出しのバグを修正します。また、他の回答にあった高速パスチェックと入力検証も追加します。要するに、これはthe answer:

using System;
using System.IO;

namespace ConsoleApp4
{
    class Program
    {
        static void Main(string[] args)
        {
            var fi1 = new FileInfo(args[0]);
            var fi2 = new FileInfo(args[1]);
            Console.WriteLine(FilesContentsAreEqual(fi1, fi2));
        }

        public static bool FilesContentsAreEqual(FileInfo fileInfo1, FileInfo fileInfo2)
        {
            if (fileInfo1 == null)
            {
                throw new ArgumentNullException(nameof(fileInfo1));
            }

            if (fileInfo2 == null)
            {
                throw new ArgumentNullException(nameof(fileInfo2));
            }

            if (string.Equals(fileInfo1.FullName, fileInfo2.FullName, StringComparison.OrdinalIgnoreCase))
            {
                return true;
            }

            if (fileInfo1.Length != fileInfo2.Length)
            {
                return false;
            }
            else
            {
                using (var file1 = fileInfo1.OpenRead())
                {
                    using (var file2 = fileInfo2.OpenRead())
                    {
                        return StreamsContentsAreEqual(file1, file2);
                    }
                }
            }
        }

        private static int ReadFullBuffer(Stream stream, byte[] buffer)
        {
            int bytesRead = 0;
            while (bytesRead < buffer.Length)
            {
                int read = stream.Read(buffer, bytesRead, buffer.Length - bytesRead);
                if (read == 0)
                {
                    // Reached end of stream.
                    return bytesRead;
                }

                bytesRead += read;
            }

            return bytesRead;
        }

        private static bool StreamsContentsAreEqual(Stream stream1, Stream stream2)
        {
            const int bufferSize = 1024 * sizeof(Int64);
            var buffer1 = new byte[bufferSize];
            var buffer2 = new byte[bufferSize];

            while (true)
            {
                int count1 = ReadFullBuffer(stream1, buffer1);
                int count2 = ReadFullBuffer(stream2, buffer2);

                if (count1 != count2)
                {
                    return false;
                }

                if (count1 == 0)
                {
                    return true;
                }

                int iterations = (int)Math.Ceiling((double)count1 / sizeof(Int64));
                for (int i = 0; i < iterations; i++)
                {
                    if (BitConverter.ToInt64(buffer1, i * sizeof(Int64)) != BitConverter.ToInt64(buffer2, i * sizeof(Int64)))
                    {
                        return false;
                    }
                }
            }
        }
    }
}

または、非常に素晴らしいものにしたい場合は、非同期バリアントを使用できます。

using System;
using System.IO;
using System.Threading.Tasks;

namespace ConsoleApp4
{
    class Program
    {
        static void Main(string[] args)
        {
            var fi1 = new FileInfo(args[0]);
            var fi2 = new FileInfo(args[1]);
            Console.WriteLine(FilesContentsAreEqualAsync(fi1, fi2).GetAwaiter().GetResult());
        }

        public static async Task<bool> FilesContentsAreEqualAsync(FileInfo fileInfo1, FileInfo fileInfo2)
        {
            if (fileInfo1 == null)
            {
                throw new ArgumentNullException(nameof(fileInfo1));
            }

            if (fileInfo2 == null)
            {
                throw new ArgumentNullException(nameof(fileInfo2));
            }

            if (string.Equals(fileInfo1.FullName, fileInfo2.FullName, StringComparison.OrdinalIgnoreCase))
            {
                return true;
            }

            if (fileInfo1.Length != fileInfo2.Length)
            {
                return false;
            }
            else
            {
                using (var file1 = fileInfo1.OpenRead())
                {
                    using (var file2 = fileInfo2.OpenRead())
                    {
                        return await StreamsContentsAreEqualAsync(file1, file2).ConfigureAwait(false);
                    }
                }
            }
        }

        private static async Task<int> ReadFullBufferAsync(Stream stream, byte[] buffer)
        {
            int bytesRead = 0;
            while (bytesRead < buffer.Length)
            {
                int read = await stream.ReadAsync(buffer, bytesRead, buffer.Length - bytesRead).ConfigureAwait(false);
                if (read == 0)
                {
                    // Reached end of stream.
                    return bytesRead;
                }

                bytesRead += read;
            }

            return bytesRead;
        }

        private static async Task<bool> StreamsContentsAreEqualAsync(Stream stream1, Stream stream2)
        {
            const int bufferSize = 1024 * sizeof(Int64);
            var buffer1 = new byte[bufferSize];
            var buffer2 = new byte[bufferSize];

            while (true)
            {
                int count1 = await ReadFullBufferAsync(stream1, buffer1).ConfigureAwait(false);
                int count2 = await ReadFullBufferAsync(stream2, buffer2).ConfigureAwait(false);

                if (count1 != count2)
                {
                    return false;
                }

                if (count1 == 0)
                {
                    return true;
                }

                int iterations = (int)Math.Ceiling((double)count1 / sizeof(Int64));
                for (int i = 0; i < iterations; i++)
                {
                    if (BitConverter.ToInt64(buffer1, i * sizeof(Int64)) != BitConverter.ToInt64(buffer2, i * sizeof(Int64)))
                    {
                        return false;
                    }
                }
            }
        }
    }
}
2
Andrew Arnott

私の実験では、Stream.ReadByte()の呼び出し回数を減らすことが間違いなく役立つことを示していますが、BitConverterを使用してバイトをパッケージ化しても、バイト配列内のバイトの比較に対して大きな違いはありません。

したがって、上記のコメントの「Math.Ceiling and iterations」ループを最も単純なループに置き換えることができます。

            for (int i = 0; i < count1; i++)
            {
                if (buffer1[i] != buffer2[i])
                    return false;
            }

比較する前にBitConverter.ToInt64が少し作業(引数を確認してからビットシフトを実行)する必要があるという事実に関係していると思います。 。

2
romeok

2つのファイルのみを比較する必要がある場合は、最速の方法になると思います(Cでは、.NETに適用できるかどうかわかりません)

  1. 両方のファイルf1、f2を開きます
  2. それぞれのファイル長l1、l2を取得します
  3. l1!= l2の場合、ファイルは異なります。やめる
  4. mmap()両方のファイル
  5. mmap()edファイルでmemcmp()を使用します

OTOH、N個のファイルのセットに重複ファイルがあるかどうかを確認する必要がある場合、間違いなくハッシュを使用してN方向のビットごとの比較を避けることです。

1
CAFxX

同じ長さの大きなファイルのもう1つの改善点は、ファイルを順番に読み取らず、ランダムブロックを多少比較することです。

複数のスレッドを使用して、ファイル内の異なる位置から開始し、前方または後方のいずれかを比較できます。

このようにして、ファイルの中央/末尾で変更を検出できます。これは、シーケンシャルアプローチを使用してそこに到達するよりも高速です。

1
Thomas Kjørnes

2つのファイル(または2つのストリーム)に同一のデータが含まれているかどうかを判断できるユーティリティ関数を次に示します。

タスクを使用して異なるスレッドでバイト配列(各ファイルで読み込まれたものから満たされた各バッファー)を比較するため、マルチスレッドの「高速」バージョンを提供しました。

予想どおり、はるかに高速(約3倍高速)ですが、CPU(マルチスレッド化されているため)とメモリ(比較スレッドごとに2バイトの配列バッファーが必要なため)を消費します。

    public static bool AreFilesIdenticalFast(string path1, string path2)
    {
        return AreFilesIdentical(path1, path2, AreStreamsIdenticalFast);
    }

    public static bool AreFilesIdentical(string path1, string path2)
    {
        return AreFilesIdentical(path1, path2, AreStreamsIdentical);
    }

    public static bool AreFilesIdentical(string path1, string path2, Func<Stream, Stream, bool> areStreamsIdentical)
    {
        if (path1 == null)
            throw new ArgumentNullException(nameof(path1));

        if (path2 == null)
            throw new ArgumentNullException(nameof(path2));

        if (areStreamsIdentical == null)
            throw new ArgumentNullException(nameof(path2));

        if (!File.Exists(path1) || !File.Exists(path2))
            return false;

        using (var thisFile = new FileStream(path1, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
        {
            using (var valueFile = new FileStream(path2, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
            {
                if (valueFile.Length != thisFile.Length)
                    return false;

                if (!areStreamsIdentical(thisFile, valueFile))
                    return false;
            }
        }
        return true;
    }

    public static bool AreStreamsIdenticalFast(Stream stream1, Stream stream2)
    {
        if (stream1 == null)
            throw new ArgumentNullException(nameof(stream1));

        if (stream2 == null)
            throw new ArgumentNullException(nameof(stream2));

        const int bufsize = 80000; // 80000 is below LOH (85000)

        var tasks = new List<Task<bool>>();
        do
        {
            // consumes more memory (two buffers for each tasks)
            var buffer1 = new byte[bufsize];
            var buffer2 = new byte[bufsize];

            int read1 = stream1.Read(buffer1, 0, buffer1.Length);
            if (read1 == 0)
            {
                int read3 = stream2.Read(buffer2, 0, 1);
                if (read3 != 0) // not eof
                    return false;

                break;
            }

            // both stream read could return different counts
            int read2 = 0;
            do
            {
                int read3 = stream2.Read(buffer2, read2, read1 - read2);
                if (read3 == 0)
                    return false;

                read2 += read3;
            }
            while (read2 < read1);

            // consumes more cpu
            var task = Task.Run(() =>
            {
                return IsSame(buffer1, buffer2);
            });
            tasks.Add(task);
        }
        while (true);

        Task.WaitAll(tasks.ToArray());
        return !tasks.Any(t => !t.Result);
    }

    public static bool AreStreamsIdentical(Stream stream1, Stream stream2)
    {
        if (stream1 == null)
            throw new ArgumentNullException(nameof(stream1));

        if (stream2 == null)
            throw new ArgumentNullException(nameof(stream2));

        const int bufsize = 80000; // 80000 is below LOH (85000)
        var buffer1 = new byte[bufsize];
        var buffer2 = new byte[bufsize];

        var tasks = new List<Task<bool>>();
        do
        {
            int read1 = stream1.Read(buffer1, 0, buffer1.Length);
            if (read1 == 0)
                return stream2.Read(buffer2, 0, 1) == 0; // check not eof

            // both stream read could return different counts
            int read2 = 0;
            do
            {
                int read3 = stream2.Read(buffer2, read2, read1 - read2);
                if (read3 == 0)
                    return false;

                read2 += read3;
            }
            while (read2 < read1);

            if (!IsSame(buffer1, buffer2))
                return false;
        }
        while (true);
    }

    public static bool IsSame(byte[] bytes1, byte[] bytes2)
    {
        if (bytes1 == null)
            throw new ArgumentNullException(nameof(bytes1));

        if (bytes2 == null)
            throw new ArgumentNullException(nameof(bytes2));

        if (bytes1.Length != bytes2.Length)
            return false;

        for (int i = 0; i < bytes1.Length; i++)
        {
            if (bytes1[i] != bytes2[i])
                return false;
        }
        return true;
    }
1
Simon Mourier

何か(うまくいけば)合理的に効率的なもの:

public class FileCompare
{
    public static bool FilesEqual(string fileName1, string fileName2)
    {
        return FilesEqual(new FileInfo(fileName1), new FileInfo(fileName2));
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="file1"></param>
    /// <param name="file2"></param>
    /// <param name="bufferSize">8kb seemed like a good default</param>
    /// <returns></returns>
    public static bool FilesEqual(FileInfo file1, FileInfo file2, int bufferSize = 8192)
    {
        if (!file1.Exists || !file2.Exists || file1.Length != file2.Length) return false;

        var buffer1 = new byte[bufferSize];
        var buffer2 = new byte[bufferSize];

        using (var stream1 = file1.Open(FileMode.Open, FileAccess.Read, FileShare.Read))
        {
            using (var stream2 = file2.Open(FileMode.Open, FileAccess.Read, FileShare.Read))
            {

                while (true)
                {
                    var bytesRead1 = stream1.Read(buffer1, 0, bufferSize);
                    var bytesRead2 = stream2.Read(buffer2, 0, bufferSize);

                    if (bytesRead1 != bytesRead2) return false;
                    if (bytesRead1 == 0) return true;
                    if (!ArraysEqual(buffer1, buffer2, bytesRead1)) return false;
                }
            }
        }
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="array1"></param>
    /// <param name="array2"></param>
    /// <param name="bytesToCompare"> 0 means compare entire arrays</param>
    /// <returns></returns>
    public static bool ArraysEqual(byte[] array1, byte[] array2, int bytesToCompare = 0)
    {
        if (array1.Length != array2.Length) return false;

        var length = (bytesToCompare == 0) ? array1.Length : bytesToCompare;
        var tailIdx = length - length % sizeof(Int64);

        //check in 8 byte chunks
        for (var i = 0; i < tailIdx; i += sizeof(Int64))
        {
            if (BitConverter.ToInt64(array1, i) != BitConverter.ToInt64(array2, i)) return false;
        }

        //check the remainder of the array, always shorter than 8 bytes
        for (var i = tailIdx; i < length; i++)
        {
            if (array1[i] != array2[i]) return false;
        }

        return true;
    }
}
1
Zar Shardan

ファイルが大きすぎない場合は、次を使用できます。

public static byte[] ComputeFileHash(string fileName)
{
    using (var stream = File.OpenRead(fileName))
        return System.Security.Cryptography.MD5.Create().ComputeHash(stream);
}

ハッシュを保存するのに役立つ場合にのみ、ハッシュを比較することが可能です。

(コードをよりクリーンなものに編集しました。)

1

バイトごとに比較するよりも「ハッシュ」の方が速いアプリケーションがあると思います。ファイルを他のファイルと比較する必要がある場合、または写真のサムネイルを変更できる場合。それはどこでどのように使用されているかに依存します。

private bool CompareFilesByte(string file1, string file2)
{
    using (var fs1 = new FileStream(file1, FileMode.Open))
    using (var fs2 = new FileStream(file2, FileMode.Open))
    {
        if (fs1.Length != fs2.Length) return false;
        int b1, b2;
        do
        {
            b1 = fs1.ReadByte();
            b2 = fs2.ReadByte();
            if (b1 != b2 || b1 < 0) return false;
        }
        while (b1 >= 0);
    }
    return true;
}

private string HashFile(string file)
{
    using (var fs = new FileStream(file, FileMode.Open))
    using (var reader = new BinaryReader(fs))
    {
        var hash = new SHA512CryptoServiceProvider();
        hash.ComputeHash(reader.ReadBytes((int)file.Length));
        return Convert.ToBase64String(hash.Hash);
    }
}

private bool CompareFilesWithHash(string file1, string file2)
{
    var str1 = HashFile(file1);
    var str2 = HashFile(file2);
    return str1 == str2;
}

ここでは、最速のものを取得できます。

var sw = new Stopwatch();
sw.Start();
var compare1 = CompareFilesWithHash(receiveLogPath, logPath);
sw.Stop();
Debug.WriteLine(string.Format("Compare using Hash {0}", sw.ElapsedTicks));
sw.Reset();
sw.Start();
var compare2 = CompareFilesByte(receiveLogPath, logPath);
sw.Stop();
Debug.WriteLine(string.Format("Compare byte-byte {0}", sw.ElapsedTicks));

オプションで、ハッシュをデータベースに保存できます。

これが役立つことを願っています

0
antonio

@chshから派生したさらに別の答え。ファイルの使用とショートカットが同じ、ファイルが存在せず、長さが異なるMD5:

/// <summary>
/// Performs an md5 on the content of both files and returns true if
/// they match
/// </summary>
/// <param name="file1">first file</param>
/// <param name="file2">second file</param>
/// <returns>true if the contents of the two files is the same, false otherwise</returns>
public static bool IsSameContent(string file1, string file2)
{
    if (file1 == file2)
        return true;

    FileInfo file1Info = new FileInfo(file1);
    FileInfo file2Info = new FileInfo(file2);

    if (!file1Info.Exists && !file2Info.Exists)
       return true;
    if (!file1Info.Exists && file2Info.Exists)
        return false;
    if (file1Info.Exists && !file2Info.Exists)
        return false;
    if (file1Info.Length != file2Info.Length)
        return false;

    using (FileStream file1Stream = file1Info.OpenRead())
    using (FileStream file2Stream = file2Info.OpenRead())
    { 
        byte[] firstHash = MD5.Create().ComputeHash(file1Stream);
        byte[] secondHash = MD5.Create().ComputeHash(file2Stream);
        for (int i = 0; i < firstHash.Length; i++)
        {
            if (i>=secondHash.Length||firstHash[i] != secondHash[i])
                return false;
        }
        return true;
    }
}
0
Andrew Taylor