Как быстро сравнить 2 файла с помощью .NET? - PullRequest
121 голосов
/ 31 августа 2009

Типичные подходы рекомендуем прочитать двоичный файл с помощью FileStream и сравнить его побайтово.

  • Будет ли сравнение контрольной суммы, такое как CRC, быстрее?
  • Существуют ли библиотеки .NET, которые могут генерировать контрольную сумму для файла?

Ответы [ 18 ]

121 голосов
/ 01 сентября 2009

Самый медленный из возможных методов - сравнивать два файла побайтно. Самое быстрое, что я смог придумать, - это аналогичное сравнение, но вместо одного байта за раз вы бы использовали массив байтов размером в 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;
    }

В моем тестировании я смог увидеть, как это превзошло простой сценарий ReadByte () почти 3: 1. В среднем за 1000 прогонов я получил этот метод на 1063 мс, а метод ниже (прямое побайтное сравнение) на 3031 мс. Хэширование всегда возвращалось на долю секунды ниже среднего значения в 865 мс. Это тестирование проводилось с видеофайлом ~ 100 МБ.

Вот методы 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;
    }
107 голосов
/ 31 августа 2009

Сравнение контрольной суммы, скорее всего, будет медленнее, чем байтовое сравнение.

Чтобы сгенерировать контрольную сумму, вам нужно загрузить каждый байт файла и выполнить его обработку. Затем вам нужно будет сделать это на втором файле. Обработка почти наверняка будет медленнее, чем проверка сравнения.

Что касается генерации контрольной суммы: это можно легко сделать с помощью классов криптографии. Вот краткий пример генерации контрольной суммы MD5 с C #.

Однако, контрольная сумма может быть быстрее и имеет больше смысла, если вы можете предварительно вычислить контрольную сумму «тестового» или «базового» случая. Если у вас есть существующий файл, и вы проверяете, совпадает ли новый файл с существующим, предварительное вычисление контрольной суммы в вашем «существующем» файле будет означать, что DiskIO потребуется только один раз, новый файл. Это, вероятно, будет быстрее, чем побайтовое сравнение.

36 голосов
/ 20 марта 2016

Если вы d̲o̲ решите, что вам действительно нужно полное побайтное сравнение (см. Другие ответы для обсуждения хэширования), тогда однострочное решение:

bool filesAreEqual = File.ReadAllBytes(path1).SequenceEqual(File.ReadAllBytes(path2));

В отличие от некоторых других опубликованных ответов, это работает корректно для любого типа файла: двоичного, текстового, мультимедийного, исполняемого и т. Д., Но в качестве полного двоичного сравнения , файлы, которые отличаются только"неважными" способами (такими как BOM , конец строки , кодировка символов , медиа-метаданные, пробелы, отступы, комментарии исходного кода и т. д.) всегда будут считаться не равными .

Этот код полностью загружает оба файла в память, поэтому его не следует использовать для сравнения гигантских файлов. Помимо этого соображения, полная загрузка на самом деле не штраф; фактически это может быть оптимальное решение .NET для размеров файлов, которые, как ожидается, будут меньше 85K , поскольку небольшие выделения в .NET очень дешевы, а приведенный выше код максимально делегирует производительность и оптимизацию файлов CLR / BCL.

Кроме того, для таких рабочих сценариев беспокойство по поводу производительности побайтного сравнения с помощью LINQ перечислителей (как показано здесь) не имеет значения, поскольку попадание на диск a̲t̲ a̲l̲l̲ для файла I / O на несколько порядков превзойдет преимущества различных альтернатив сравнения памяти. Например, хотя SequenceEqual действительно фактически дает нам «оптимизацию» отказа от первого несоответствия , это вряд ли имеет значение после того, как вы уже извлекли содержимое файлов, каждый из которых был полностью необходим чтобы подтвердить совпадение ..

С другой стороны, приведенный выше код не включает , включающий готовый прерывание для файлов различного размера , которые могут обеспечить ощутимую (возможно, измеримую) производительность разница. Это реально, потому что, хотя длина файла доступна в структуре WIN32_FILE_ATTRIBUTE_DATA (которая в любом случае должна быть извлечена первой для любого доступа к файлу), продолжение доступа к содержимому файла требует совершенно другой выборки, которую потенциально можно избежать. Если вы обеспокоены этим, решение становится двумя строками:

// slight optimization over the code shown above
bool filesAreEqual = new FileInfo(path1).Length == new FileInfo(path2).Length && 
       File.ReadAllBytes(path1).SequenceEqual(File.ReadAllBytes(path2));

Вы также можете расширить это, чтобы избежать вторичных выборок, если оба (эквивалентные) значения Length оба найдены равными нулю (не показаны), и / или чтобы избежать построения каждого FileInfo дважды (также не показано).

33 голосов
/ 31 августа 2009

В дополнение к ответу Рида Копси :

  • Худший случай, когда два файла идентичны. В этом случае лучше сравнить файлы побайтно.

  • Если два файла не идентичны, вы можете немного ускорить процесс, обнаружив, что они не идентичны.

Например, если два файла имеют разную длину, вы знаете, что они не могут быть идентичными, и вам даже не нужно сравнивать их фактическое содержимое.

15 голосов
/ 14 апреля 2010

Становится еще быстрее, если вы не читаете небольшие 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 голосов
/ 31 августа 2009

Единственное, что может сделать сравнение контрольной суммы немного быстрее, чем побайтное сравнение, - это то, что вы читаете один файл за раз, что несколько сокращает время поиска для головки диска. Этот небольшой выигрыш, однако, вполне может быть поглощен дополнительным временем вычисления хеша.

Кроме того, сравнение контрольной суммы, конечно, имеет шанс быть более быстрым, если файлы идентичны. Если это не так, побайтовое сравнение заканчивается первым различием, что делает его намного быстрее.

Вам также следует учитывать, что сравнение хеш-кода говорит только о том, что очень вероятно , что файлы идентичны. Чтобы быть на 100% уверенным, вам нужно проводить побайтовое сравнение.

Если, например, хэш-код равен 32 битам, вы примерно на 99,999999998% уверены, что файлы идентичны, если хеш-коды совпадают. Это близко к 100%, но если вам действительно нужна 100% уверенность, это не так.

11 голосов
/ 31 августа 2009

Редактировать: Этот метод будет не работать для сравнения двоичных файлов!

В .NET 4.0 класс File имеет следующие два новых метода:

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));
6 голосов
/ 27 января 2012

Честно говоря, я думаю, вам нужно как можно меньше обрезать свое дерево поиска.

Что нужно проверить перед переходом побайтно:

  1. Размеры одинаковы?
  2. Последний байт в файле A отличается от файла B

Кроме того, одновременное чтение больших блоков будет более эффективным, поскольку накопители будут читать последовательные байты быстрее. Байт-байт приводит не только к большему количеству системных вызовов, но и к тому, что головка чтения традиционного жесткого диска выполняет поиск назад и вперед, если оба файла находятся на одном диске.

Считайте блок A и блок B в байтовый буфер и сравните их (НЕ используйте Array.Equals, см. Комментарии). Настраивайте размер блоков, пока не добьетесь того, что вы считаете хорошим компромиссом между памятью и производительностью. Вы также можете многопоточное сравнение, но не многопоточное чтение диска.

2 голосов
/ 16 сентября 2011

Мои эксперименты показывают, что это определенно помогает вызывать Stream.ReadByte () меньше раз, но использование BitConverter для упаковки байтов не имеет большого значения для сравнения байтов в байтовом массиве.

Таким образом, можно заменить этот цикл «Math.Ceiling and iterations» в комментарии выше на самый простой:

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

Полагаю, это связано с тем, что BitConverter.ToInt64 необходимо выполнить небольшую работу (проверить аргументы, а затем выполнить сдвиг битов) перед сравнением, и это в конечном итоге будет таким же объемом работы, что и сравнение 8 байтов в двух массивах.

2 голосов
/ 21 ноября 2017

Мой ответ является производным от @lars, но исправляет ошибку в вызове Stream.Read. Я также добавил быструю проверку пути, которую имели другие ответы, и проверку ввода. Короче говоря, это должен быть ответ :

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;
                    }
                }
            }
        }
    }
}
...