Як порівняти 2 файли швидко за допомогою .NET?


136

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

  • Чи порівняння контрольної суми, наприклад, CRC, буде швидшим?
  • Чи є бібліотеки .NET, які можуть генерувати контрольну суму для файлу?

Відповіді:


117

Порівняння контрольної суми, швидше за все, буде повільніше, ніж порівняння байт-байт.

Щоб створити контрольну суму, вам потрібно буде завантажити кожен байт файлу і виконати обробку на ньому. Тоді вам доведеться це зробити у другому файлі. Обробка майже напевно буде повільнішою, ніж перевірка порівняння.

Щодо генерування контрольної суми: Ви можете це легко зробити з класами криптографії. Ось короткий приклад генерації контрольної суми MD5 за допомогою C #.

Однак контрольна сума може бути швидшою і мати більше сенсу, якщо ви можете попередньо обчислити контрольну суму з "тестового" або "базового" випадку. Якщо у вас є існуючий файл, і ви перевіряєте, чи є новий файл таким же, як і існуючий, попереднє обчислення контрольної суми у вашому "існуючому" файлі означатиме лише необхідність робити DiskIO один раз, на новий файл. Це, швидше за все, буде швидше порівняння байт-байт.


30
Не забудьте врахувати, де розміщені ваші файли. Якщо ви порівнюєте локальні файли із резервною копією на півдорозі по всьому світу (або через мережу з жахливою пропускною здатністю), вам може бути краще спочатку хеш і надіслати контрольну суму по мережі, а не надсилати потік байтів порівняти.
Кім

@ReedCopsey: У мене є аналогічна проблема, оскільки мені потрібно зберігати вхідні / вихідні файли, створені кількома розробками, які, як передбачається, містять багато дублювань. Я думав використовувати попередньо обчислений хеш, але чи вважаєте ви, що я можу обгрунтовано припустити, що якщо 2 (наприклад, MD5) хеш рівні, два файли рівні, і уникнути подальшого порівняння байт-2-байт? Наскільки мені відомо, зіткнення MD5 / SHA1 і т.
Д.

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

1
Ти полегшив мені життя. Дякую
anindis

2
@anindis: для повноти ви можете прочитати відповідь @Lars та @ RandomInsano . Радий, що допомагав так багато років! :)
чш

1
FilesAreEqual_HashМетод повинен мати usingна обох потоків файлу теж як ReadByteметод в іншому випадку він буде висіти на обох файлах.
Ян Мерсер

2
Зауважте, що FileStream.Read()насправді може бути прочитано менше байт, ніж запитуване число. Ви повинні використовувати StreamReader.ReadBlock()замість цього.
Палець

2
У версії Int64, коли довжина потоку не кратна Int64, тоді остання ітерація порівнює незаповнені байти за допомогою заповнення попередньої ітерації (яка також повинна бути рівною, щоб це було добре). Також якщо довжина потоку менше sizeof (Int64), то невиконані байти дорівнюють 0, оскільки C # ініціалізує масиви. IMO, код, ймовірно, повинен коментувати ці дивацтва.
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));


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

Цей код повністю завантажує обидва файли в пам'ять, тому його не слід використовувати для порівняння справді гігантських файлів . Крім цього важливого застереження, повне завантаження насправді не є штрафом з огляду на дизайн .NET GC (оскільки він принципово оптимізований для зберігання невеликих, короткочасних виділень надзвичайно дешево ), а насправді навіть може бути оптимальним, коли очікуються розміри файлів бути менше , ніж 8ки , тому що , використовуючи мінімум коду користувача (як показано тут) передбачає максимально делегування проблеми продуктивності файлу в CLR, BCLі JITна користь від (наприклад) за останнім словом техніки проектування, системний коду, а також адаптивної оптимізації часу виконання.

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


3
цей файл не виглядає добре для великих файлів. не годиться для використання пам'яті, оскільки вона буде читати обидва файли до кінця, перш ніж починати порівнювати байтовий масив. Ось чому я б швидше перейшов на потокову програму з буфером.
Krypto_47

3
@ Krypto_47 Я обговорював ці фактори та відповідне використання у тексті своєї відповіді.
Гленн Слейден

33

Окрім відповіді Рід Копсі :

  • Найгірший випадок, коли два файли однакові. У цьому випадку найкраще порівнювати файли по байтах.

  • Якщо два файли не однакові, ви можете трохи прискорити роботу, виявивши швидше, що вони не однакові.

Наприклад, якщо два файли різної довжини, то ви знаєте, що вони не можуть бути однаковими, і вам навіть не доведеться порівнювати їх фактичний вміст.


10
Для завершення: інший великий виграш припиняється, як тільки байти в 1 позиції відрізняються.
Хенк Холтерман

6
@Henk: Я думав, що це занадто очевидно :-)
dtb

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

13
Загалом чек count1 != count2невірний. Stream.Read()з різних причин може повернути менший за вказаний вами рахунок.
porges

1
Для того, щоб гарантувати , що буфер буде тримати парне число Int64блоків, ви можете вирахувати розмір , як це: const int bufferSize = 1024 * sizeof(Int64).
Джек А.

14

Єдине, що може зробити порівняння контрольної суми трохи швидшим, ніж порівняння байт-байт, - це той факт, що ви читаєте один файл за один раз, дещо скорочуючи час пошуку для голови диска. Однак цей невеликий приріст може бути з'їдений за час, що розраховується хеш.

Крім того, порівняння контрольної суми, звичайно, має лише шанс бути швидшим, якщо файли однакові. Якщо їх немає, порівняння байтів за байтом закінчилось би першою різницею, зробивши це набагато швидше.

Слід також врахувати, що порівняння хеш-коду говорить лише про те, що цілком ймовірно, що файли однакові. Щоб бути на 100% впевненим, вам потрібно провести порівняння байт-байт.

Якщо хеш-код, наприклад, становить 32 біти, ви приблизно на 99,99999998% впевнені, що файли однакові, якщо хеш-коди збігаються. Це близько 100%, але якщо вам справді потрібна 100% впевненість, це не все.


Використовуйте більший хеш, і ви можете отримати шанси на помилковий позитив набагато нижче шансів, на яких помилився комп'ютер під час тесту.
Лорен Печтел

Я не згоден з приводу хеш-часу проти пошуку часу. Ви можете зробити багато обчислень під час однієї голівки. Якщо шанси великі, що файли відповідають, я б використав хеш з великою кількістю біт. Якщо є розумний шанс на матч, я б порівнював їх за один раз, скажімо, на 1 МБ блоків. (Виберіть розмір блоку, який розділить 4k рівномірно, щоб ви ніколи не розділили сектори.)
Лорен Печтел,

1
Щоб пояснити цифру @ Guffa 99,99999998%, це походить від обчислень 1 - (1 / (2^32)), що є ймовірністю того, що будь-який один файл матиме якийсь 32-бітний хеш. Ймовірність того, що два різних файли мають один і той же хеш, однакова, оскільки перший файл надає "задане" хеш-значення, і нам потрібно лише врахувати, чи відповідає інший файл цьому значенню чи ні. Шанси на 64- та 128-бітне хешування зменшуються до 99,999999999999999994% та 99,9999999999999999999999999999999999997% (відповідно), ніби це має значення з такими незрівнянними числами.
Гленн Слейден

... Дійсно, той факт, що для цих людей більшість людей важче зрозуміти, ніж імовірно просте поняття, хоча істинне, про "нескінченно багато файлів, що стикаються в один і той же хеш-код", може пояснити, чому люди безпідставно підозріли прийняти хеш-аси рівність.
Гленн Слейден

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

Зауважте, що у файлу також є функція ReadAllBytes, яка також може використовувати SequenceEquals, тому використовуйте його, як він би працював на всіх файлах. І як сказав @RandomInsano, це зберігається в пам'яті, тому, хоча це ідеально добре використовувати для невеликих файлів, я був би обережний, використовуючи його з великими файлами.
DaedalusAlpha

1
@DaedalusAlpha Він повертає безліч, тому рядки завантажуватимуться на вимогу і не зберігатимуться в пам'яті весь час. ReadAllBytes, з іншого боку, повертає весь файл у вигляді масиву.
IllidanS4 хоче, щоб Моніка повернулася

7

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

Що потрібно перевірити, перш ніж байт-байт:

  1. Чи однакові розміри?
  2. Чи останній байт у файлі A, відмінний від файлу B

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

Прочитайте фрагмент A і шматок B у байтовому буфері та порівняйте їх (НЕ використовуйте Array.Equals, див. Коментарі). Налаштуйте розмір блоків, поки ви не потрапите на те, що відчуваєте, - це хороша торгівля між пам’яттю та продуктивністю. Ви також можете багатопотокове порівняння, але не мультипотокове зчитування диска.


Використання Array.Equals - це погана ідея, оскільки вона порівнює весь масив. Ймовірно, щонайменше один зчитуваний блок не заповнить весь масив.
Doug Clutter

Чому порівняння всього масиву є поганою ідеєю? Чому блок читання не заповнює масив? Однозначно хороший момент налаштування, але саме тому ви граєте з розмірами. Додаткові бали для порівняння в окремій нитці.
RandomInsano

Коли ви визначаєте байтовий масив, він матиме фіксовану довжину. (наприклад - var buffer = new byte [4096]) Коли ви читаєте блок з файлу, він може або не може повернути цілі 4096 байтів. Наприклад, якщо файл довжиною всього 3000 байт.
Doug Clutter

Ах, тепер я розумію! Хороша новина - читання поверне кількість байтів, завантажених у масив, тому якщо масив не вдасться заповнити, дані будуть. Оскільки ми перевіряємо рівність, старі дані буфера не матимуть значення. Документи: msdn.microsoft.com/en-us/library/9kstw824(v=vs.110).aspx
RandomInsano

Також важливо, моя рекомендація використовувати метод рівних () - погана ідея. У 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;
                    }
                }
            }
        }
    }
}

Або якщо ви хочете бути надзвичайним, ви можете скористатися варіантом async:

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

не хотів би біт-конвертер бути кращим як `` `для (var i = 0; i <count; i + = sizeof (довгий)) {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 байтів у двох масивах .


1
Array.Equals заглиблюється в систему, тому, швидше за все, це буде набагато швидше, ніж перейти на байт за байтом у C #. Я не можу говорити за Microsoft, але в глибині душі Mono використовує команду memcpy () C для рівності масиву. Неможливо отримати набагато швидше.
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 () ed

ОТОХ, якщо вам потрібно знайти, чи є у наборі N файлів повторювані файли, то найшвидший спосіб, безсумнівно, використовується хеш, щоб уникнути N-шлях біт-біт порівняння.


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 мали б різну довжину?
frogpelt

-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)));
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.