Как быстро сравнить 2 файла с помощью .NET?

136

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

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

Ответы:

117

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

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

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

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

Рид Копси
источник
30
Обязательно примите во внимание, где находятся ваши файлы. Если вы сравниваете локальные файлы с резервной копией на полпути по всему миру (или по сети с ужасной пропускной способностью), вам может быть лучше сначала хешировать и отправлять контрольную сумму по сети, а не посылать поток байтов сравнить.
Ким
@ReedCopsey: У меня похожая проблема, так как мне нужно хранить входные / выходные файлы, созданные несколькими разработками, которые должны содержать много дубликатов. Я думал использовать предварительно вычисленный хеш, но вы думаете, что я могу разумно предположить, что если 2 (например, MD5) хеша равны, 2 файла равны и избегают дальнейшего байтового 2-байтового сравнения? Насколько я знаю, столкновения MD5 / SHA1 и т. Д. Действительно маловероятны ...
digEmAll
1
@digEmAll Вероятность столкновения низка - вы всегда можете сделать более сильный хеш - то есть: используйте SHA256 вместо SHA1, что еще больше уменьшит вероятность столкновений.
Рид Копси
спасибо за ваш ответ - я просто вхожу в .net. Я предполагаю, что если кто-то использует технику хэш-кода / контрольной суммы, то где-то будут постоянно храниться хеши основной папки? из любопытства, как бы вы сохранили его для приложения WPF - что бы вы сделали? (В настоящее время я смотрю на XML, текстовые файлы или базы данных).
BKSpurgeon
139

Самый медленный способ - это сравнить два файла побайтно. Самое быстрое, что я смог придумать, - это аналогичное сравнение, но вместо одного байта за раз вы бы использовали массив байтов размером в 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;
    }
CHSH
источник
1
Ты сделал мою жизнь проще. Спасибо
Anindis
2
@anindis: Для полноты, вы можете прочитать и ответить @Lars' и @ ответ RandomInsano в . Рад, что это помогло так много лет! :)
чш
1
FilesAreEqual_HashМетод должен иметь usingна обоих потоков файла тоже как ReadByteметод в противном случае он будет висеть на обоих файлах.
Ян Мерсер
2
Обратите внимание, что на FileStream.Read()самом деле может быть прочитано меньше байтов, чем запрошенное число. Вы должны использовать StreamReader.ReadBlock()вместо этого.
Palec
2
В версии Int64, когда длина потока не кратна Int64, последняя итерация сравнивает незаполненные байты, используя заполнение предыдущей итерации (которое также должно быть равно, так что это нормально). Также, если длина потока меньше sizeof (Int64), то незаполненные байты равны 0, поскольку C # инициализирует массивы. ИМО, код, вероятно, должен комментировать эти странности.
crokusek
46

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


• для System.IO.FileInfoслучаев:

public static bool AreFileContentsEqual(FileInfo fi1, FileInfo fi2) =>
    fi1.Length == fi2.Length &&
    (fi1.Length == 0 || File.ReadAllBytes(fi1.FullName).SequenceEqual(
                        File.ReadAllBytes(fi2.FullName)));


• для System.Stringимен путей:

public static bool AreFileContentsEqual(String path1, String path2) =>
                   AreFileContentsEqual(new FileInfo(path1), new FileInfo(path2));


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

Этот код полностью загружает оба файла в память, поэтому его не следует использовать для сравнения действительно гигантских файлов . Помимо этого важного предостережения, полная загрузка на самом деле не является штрафом, учитывая дизайн .NET GC (потому что он принципиально оптимизирован, чтобы сохранять небольшие, недолговечные выделения очень дешевыми ), и на самом деле может даже быть оптимальным, когда ожидаются размеры файлов быть меньше , чем 8кИ , потому что , используя минимум кода пользователя (как показано здесь) предполагает максимально делегирование проблемы производительности файла в CLR, BCLи JITна пользу от (например) по последнему слову техники проектирования, системный кода, а также адаптивной оптимизации времени выполнения.

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

Гленн Слэйден
источник
3
этот не выглядит хорошо для больших файлов. не подходит для использования памяти, так как он прочитает оба файла до конца перед началом сравнения байтового массива. Вот почему я предпочел бы пойти для потокового чтения с буфером.
Krypto_47
3
@ Krypto_47 Я обсуждал эти факторы и примерное использование в тексте моего ответа.
Гленн
33

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

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

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

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

DTB
источник
10
Для завершения: другой большой выигрыш останавливается, как только байты в 1 позиции различаются.
Хенк Холтерман
6
@Henk: я думал, что это было слишком очевидно :-)
дтб
1
Хороший момент при добавлении этого. Это было очевидно для меня, поэтому я не включил это, но это хорошо, чтобы упомянуть.
Рид Копси
16

Это становится еще быстрее, если вы не читаете небольшие 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;
                }
            }
        }
    }
}
Lars
источник
13
В общем, проверка count1 != count2не верна. Stream.Read()может вернуть меньше, чем количество, которое вы предоставили, по разным причинам.
Porges
1
Для того, чтобы гарантировать , что буфер будет держать четное число Int64блоков, вы можете вычислить размер , как это: const int bufferSize = 1024 * sizeof(Int64).
Джек А.
14

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

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

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

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

Guffa
источник
Используйте больший хэш, и вы можете получить вероятность ложного срабатывания намного ниже вероятности ошибки компьютера во время теста.
Лорен Печтель
Я не согласен по поводу времени хеширования против времени поиска. Вы можете сделать много вычислений во время одного поиска головы. Если вероятность того, что файлы совпадают, высока, я бы использовал хэш с большим количеством битов. Если есть вероятность совпадения, я бы сравнивал их по одному блоку, например, по 1 МБ. (Выберите размер блока, который 4k делит равномерно, чтобы гарантировать, что вы никогда не разделите секторы.)
Loren Pechtel
1
Чтобы объяснить цифру @ Guffa 99.99999998%, она исходит из вычислений 1 - (1 / (2^32)), то есть вероятности того, что любой отдельный файл будет иметь определенный 32-битный хэш. Вероятность того, что два разных файла имеют одинаковый хэш, одинакова, поскольку первый файл предоставляет «заданное» значение хеш-функции, и нам нужно только рассмотреть, соответствует ли другой файл этому значению. Шансы с 64- и 128-битным хешированием уменьшаются до 99,9999999999999999994% и 99,999999999999999999999999999999999997% (соответственно), как будто это имеет значение с такими непостижимыми числами.
Гленн Слэйден
... Действительно, тот факт, что большинству людей эти цифры труднее понять, чем предположительно простое, хотя и истинное понятие «бесконечно много файлов, сталкивающихся в один и тот же хэш-код», может объяснить, почему люди необоснованно с подозрением относятся к принятию хеш- кода как равенство.
Гленн
13

Изменить: этот метод не будет работать для сравнения двоичных файлов!

В .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));
Сэм Харвелл
источник
1
@dtb: это не работает для двоичных файлов. Вы, наверное, уже печатали комментарий, когда я понял это и добавил правку вверху моего поста. : o
Сэм Харвелл
@ 280Z28: Я ничего не сказал ;-)
DTB
Разве вам не нужно хранить оба файла в памяти?
RandomInsano
Обратите внимание, что File также имеет функцию ReadAllBytes, которая также может использовать SequenceEquals, поэтому используйте ее вместо этого, поскольку она будет работать со всеми файлами. И, как сказал @RandomInsano, он хранится в памяти, поэтому, хотя он прекрасно подходит для небольших файлов, я бы осторожно использовал его для больших файлов.
DaedalusAlpha
1
@DaedalusAlpha Возвращает перечисляемое значение, поэтому строки будут загружаться по требованию и не сохраняться в памяти все время. ReadAllBytes, с другой стороны, возвращает весь файл в виде массива.
IllidanS4 хочет, чтобы Моника вернулась
7

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

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

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

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

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

RandomInsano
источник
Использование Array.Equals - плохая идея, потому что он сравнивает весь массив. Вполне вероятно, что хотя бы одно чтение блока не заполнит весь массив.
Даг Клаттер
Почему сравнение всего массива плохая идея? Почему чтение блока не заполняет массив? Определенно есть хорошая настройка, но именно поэтому вы играете с размерами. Дополнительные баллы для проведения сравнения в отдельном потоке.
RandomInsano
Когда вы определяете байтовый массив, он будет иметь фиксированную длину. (например: var buffer = new byte [4096]) Когда вы читаете блок из файла, он может возвращать или не возвращать полные 4096 байтов. Например, если файл имеет длину всего 3000 байт.
Даг Клаттер
Ах, теперь я понимаю! Хорошей новостью является то, что чтение вернет количество байтов, загруженных в массив, поэтому, если массив не может быть заполнен, будут данные. Поскольку мы проверяем на равенство, старые данные буфера не будут иметь значения. Документы: msdn.microsoft.com/en-us/library/9kstw824(v=vs.110).aspx
RandomInsano
Также важно, что моя рекомендация использовать метод Equals () - плохая идея. В Mono они сравнивают память, так как элементы являются смежными в памяти. Microsoft, однако, не отменяет его, а только делает сравнение ссылок, которое здесь всегда будет ложным.
RandomInsano
4

Мой ответ является производным от @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;
                    }
                }
            }
        }
    }
}
Эндрю Арнотт
источник
не будет ли битовый преобразователь лучше, чем `` `for (var i = 0; i <count; i + = sizeof (long)) {if (BitConverter.ToInt64 (buffer1, i)! = BitConverter.ToInt64 (buffer2, i)) {вернуть ложь; }} `` `
Симон
2

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

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

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

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

romeok
источник
1
Array.Equals идет глубже в систему, поэтому, скорее всего, будет намного быстрее, чем проходить побайтно в C #. Я не могу говорить за Microsoft, но в глубине души Mono использует команду C memcpy () для равенства массивов. Не может быть намного быстрее, чем это.
RandomInsano
2
@RandomInsano Полагаю, вы имеете в виду memcmp (), а не memcpy ()
SQL Police
1

Если файлы не слишком большие, вы можете использовать:

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

Сравнивать хэши можно только в том случае, если они полезны для хранения.

(Отредактировал код до чего-то более чистого.)

У Сесила есть имя
источник
1

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

Вы можете использовать несколько потоков, начиная с разных позиций в файле и сравнивая их вперед или назад.

Таким образом, вы можете обнаружить изменения в середине / конце файла, быстрее, чем при последовательном подходе.

Томас Кьернес
источник
1
Вызов диска может вызвать проблемы здесь?
RandomInsano
Физические диски да, SSD справились бы с этим.
TheLegendaryCopyCoder
1

Если вам нужно сравнить только два файла, я думаю, что самый быстрый способ будет (в C, я не знаю, применимо ли это к .NET)

  1. откройте оба файла f1, f2
  2. получить соответствующую длину файла l1, l2
  3. если l1! = l2 файлы разные; стоп
  4. mmap () оба файла
  5. используйте memcmp () для файлов mmap ()

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

CAFxX
источник
1

Что-то (надеюсь) достаточно эффективное:

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

Вот некоторые служебные функции, которые позволяют вам определить, содержат ли два файла (или два потока) идентичные данные.

Я предоставил «быструю» версию, которая является многопоточной, поскольку она сравнивает байтовые массивы (каждый буфер заполняется из того, что было прочитано в каждом файле) в разных потоках, используя Задачи.

Как и ожидалось, он намного быстрее (примерно в 3 раза быстрее), но потребляет больше ЦП (потому что он многопоточный) и больше памяти (потому что для каждого потока сравнения требуется два буфера массива байтов).

    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;
    }
Саймон Мурье
источник
0

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

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

Еще один ответ, полученный из @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;
    }
}
Эндрю Тейлор
источник
Вы говорите, if (i>=secondHash.Length ...при каких обстоятельствах два хэша MD5 будут разной длины?
лягушка
-1

Я нашел, что это хорошо работает, сравнивая сначала длину без чтения данных, а затем сравнивая прочитанную последовательность байтов

private static bool IsFileIdentical(string a, string b)
{            
   if (new FileInfo(a).Length != new FileInfo(b).Length) return false;
   return (File.ReadAllBytes(a).SequenceEqual(File.ReadAllBytes(b)));
}
kernowcode
источник