Jak szybko porównać 2 pliki za pomocą .NET?


Odpowiedzi:


117

Porównanie sum kontrolnych najprawdopodobniej będzie wolniejsze niż porównanie bajt po bajcie.

Aby wygenerować sumę kontrolną, musisz załadować każdy bajt pliku i wykonać na nim przetwarzanie. Będziesz musiał to zrobić na drugim pliku. Przetwarzanie będzie prawie na pewno wolniejsze niż kontrola porównawcza.

Jeśli chodzi o generowanie sumy kontrolnej: Możesz to łatwo zrobić za pomocą klas kryptografii. Oto krótki przykład generowania sumy kontrolnej MD5 w języku C #.

Jednak suma kontrolna może być szybsza i mieć więcej sensu, jeśli można wstępnie obliczyć sumę kontrolną przypadku „testowego” lub „podstawowego”. Jeśli masz istniejący plik i sprawdzasz, czy nowy plik jest taki sam jak istniejący, wstępne obliczenie sumy kontrolnej na „istniejącym” pliku oznaczałoby konieczność wykonania DiskIO tylko raz, na nowy plik. Prawdopodobnie byłoby to szybsze niż porównanie bajt po bajcie.


30
Pamiętaj, aby wziąć pod uwagę lokalizację Twoich plików. Jeśli porównujesz pliki lokalne z kopią zapasową w połowie świata (lub przez sieć o okropnej przepustowości), być może lepiej będzie najpierw wykonać hash i wysłać sumę kontrolną przez sieć, zamiast wysyłać strumień bajtów do porównać.
Kim

@ReedCopsey: Mam podobny problem, ponieważ muszę przechowywać pliki wejściowe / wyjściowe utworzone przez kilka opracowań, które mają zawierać wiele duplikatów. Myślałem, że użyję wstępnie obliczonego skrótu, ale czy myślisz, że mogę rozsądnie założyć, że jeśli 2 (np. MD5) hash są równe, to 2 pliki są równe i unikam dalszego porównania bajt-2-bajt? O ile wiem, kolizje MD5 / SHA1 itp. Są naprawdę mało prawdopodobne ...
digEmAll

1
@digEmAll Szansa na kolizję jest niska - jednak zawsze możesz zrobić silniejszy hash - tj .: użyj SHA256 zamiast SHA1, co jeszcze bardziej zmniejszy prawdopodobieństwo kolizji.
Reed Copsey

dziękuję za odpowiedź - właśnie wchodzę do .net. Zakładam, że jeśli ktoś używa techniki hashcode / check sum, to skróty głównego folderu będą gdzieś przechowywane? z ciekawości, jak byś to przechował dla aplikacji WPF - co byś zrobił? (Aktualnie przeglądam XML, pliki tekstowe lub bazy danych).
BKSpurgeon

139

Najwolniejszą możliwą metodą jest porównywanie dwóch plików bajt po bajcie. Najszybsze, jakie udało mi się wymyślić, to podobne porównanie, ale zamiast jednego bajtu na raz, użyjesz tablicy bajtów o rozmiarze Int64, a następnie porównasz wynikowe liczby.

Oto, co wymyśliłem:

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

W moich testach mogłem zobaczyć, że to przewyższa prosty scenariusz ReadByte () o prawie 3: 1. Uśredniony ponad 1000 uruchomień, otrzymałem tę metodę przy 1063 ms, a metodę poniżej (proste porównanie bajt po bajcie) przy 3031 ms. Haszowanie zawsze wracało poniżej sekundy, średnio około 865 ms. Ten test dotyczył pliku wideo ~ 100 MB.

Oto metody ReadByte i haszujące, których użyłem do celów porównawczych:

    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
Ułatwiłeś mi życie. Dziękuję
anindis

2
@anindis: Aby uzyskać kompletność, możesz przeczytać zarówno odpowiedź @Lars, jak i odpowiedź @ RandomInsano . Cieszę się, że pomogło to tyle lat temu! :)
chsh

1
FilesAreEqual_HashMetoda powinna mieć usingzarówno pliku strumieni zbyt podobnie jak ReadBytemetody inaczej będzie powiesić na obu plików.
Ian Mercer

2
Zauważ, że FileStream.Read()może faktycznie odczytać mniej bajtów niż żądana liczba. Zamiast tego powinieneś użyć StreamReader.ReadBlock().
Palec

2
W wersji Int64, gdy długość strumienia nie jest wielokrotnością Int64, ostatnia iteracja porównuje niewypełnione bajty przy użyciu wypełnienia poprzedniej iteracji (które również powinno być równe, więc jest w porządku). Również jeśli długość strumienia jest mniejsza niż sizeof (Int64), wówczas niewypełnione bajty mają wartość 0, ponieważ C # inicjuje tablice. IMO, kod powinien prawdopodobnie skomentować te dziwactwa.
crokusek

46

Jeśli nie zdecydujesz, że naprawdę potrzebujesz pełnego porównania bajt po bajcie (zobacz inne odpowiedzi w celu omówienia haszowania), najłatwiejszym rozwiązaniem jest:


• na System.IO.FileInfoprzykład:

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


• dla System.Stringnazw ścieżek:

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


W przeciwieństwie do innych opublikowanych odpowiedzi, jest to bezsprzecznie poprawne dla każdego rodzaju plików: binarnych, tekstowych, multimedialnych, wykonywalnych itp., Ale jako pełne porównanie binarne pliki, które różnią się tylko w „nieistotny” sposób (np. BOM , wiersz -ending , kodowanie znaków , metadane multimediów, spacje, dopełnienie, komentarze w kodzie źródłowym itp.) zawsze będą uważane za nierówne .

Ten kod ładuje oba pliki w całości do pamięci, więc nie powinien być używany do porównywania naprawdę gigantycznych plików . Poza tym ważnym zastrzeżeniem, pełne ładowanie nie jest tak naprawdę karą, biorąc pod uwagę projekt .NET GC (ponieważ jest zasadniczo zoptymalizowany, aby utrzymywać bardzo niskie , krótkotrwałe alokacje niezwykle tanie ), a w rzeczywistości może być nawet optymalne, gdy oczekuje się rozmiaru pliku powinna być mniejsza niż 85k , ponieważ przy użyciu minimum kodu użytkownika (patrz tutaj) implikuje maksymalnie delegowanie problemów z wydajnością do pliku CLR, BCLoraz JITdo korzystania z (na przykład) najnowszej technologii projektowania, kod systemu i adaptacyjnych optymalizacje uruchomieniowych.

Ponadto w takich scenariuszach dnia roboczego obawy dotyczące wydajności porównywania bajt po bajcie za pomocą modułów LINQwyliczających (jak pokazano tutaj) są dyskusyjne, ponieważ uderzenie w dysk a̲t̲ a̲l̲l̲ dla wejścia / wyjścia pliku przyćmiewa o kilka rzędów wielkości korzyści różnych alternatyw porównujących pamięć. Na przykład, choć SequenceEqual nie w rzeczywistości daje nam „Optymalizacja” z porzucenie na pierwszym niedopasowania , to nie ma znaczenia po już pobrana zawartość pliki, każdy w pełni należy potwierdzić meczu ..


3
ten nie wygląda dobrze dla dużych plików. nie jest dobre dla użycia pamięci, ponieważ odczyta oba pliki do końca przed rozpoczęciem porównywania tablicy bajtów. Dlatego wolałbym raczej wybrać streamer z buforem.
Krypto_47,

3
@ Krypto_47 Omówiłem te czynniki i odpowiednie zastosowanie w tekście mojej odpowiedzi.
Glenn Slayden,

33

Oprócz odpowiedzi Reeda Copseya :

  • W najgorszym przypadku oba pliki są identyczne. W takim przypadku najlepiej porównać pliki bajt po bajcie.

  • Jeśli dwa pliki nie są identyczne, możesz nieco przyspieszyć działanie, wykrywając wcześniej, że nie są identyczne.

Na przykład, jeśli dwa pliki mają różną długość, wiesz, że nie mogą być identyczne i nie musisz nawet porównywać ich rzeczywistej zawartości.


10
Aby zakończyć: drugi duży zysk zatrzymuje się, gdy tylko bajty na pozycji 1 są różne.
Henk Holterman

6
@Henk: Myślałem, że to zbyt oczywiste :-)
dtb

1
Dobry punkt na dodanie tego. Było to dla mnie oczywiste, więc nie uwzględniłem tego, ale warto o tym wspomnieć.
Reed Copsey

16

Robi się jeszcze szybciej, jeśli nie czytasz małych 8-bajtowych fragmentów, ale umieszczasz pętlę, odczytując większy fragment. Skróciłem średni czas porównania do 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
Generalnie kontrola count1 != count2nie jest poprawna. Stream.Read()może zwrócić mniej niż podana liczba z różnych powodów.
porge

1
Aby upewnić się, że bufor będzie posiadać numer nawet Int64bloków, można obliczyć wielkość takiego: const int bufferSize = 1024 * sizeof(Int64).
Jack A.

14

Jedyną rzeczą, która może sprawić, że porównanie sum kontrolnych będzie nieco szybsze niż porównanie bajt po bajcie, to fakt, że czytasz jeden plik na raz, co nieco skraca czas wyszukiwania głowicy dysku. Ten niewielki zysk może jednak zostać zjedzony przez dodatkowy czas obliczania hasha.

Oczywiście porównanie sum kontrolnych ma szansę być szybsze tylko wtedy, gdy pliki są identyczne. Jeśli tak nie jest, porównanie bajt po bajcie zakończyłoby się przy pierwszej różnicy, co znacznie przyspieszyło.

Należy również wziąć pod uwagę, że porównanie kodu skrótu mówi tylko, że jest to bardzo prawdopodobne że pliki są identyczne. Aby mieć 100% pewności, musisz porównać bajt po bajcie.

Jeśli na przykład kod skrótu ma 32 bity, masz około 99,999999998% pewności, że pliki są identyczne, jeśli kody skrótu są zgodne. To prawie 100%, ale jeśli naprawdę potrzebujesz 100% pewności, to nie wszystko.


Użyj większego skrótu, a prawdopodobieństwo fałszywie dodatniego wyniku będzie znacznie niższe niż prawdopodobieństwo, że komputer popełnił błąd podczas wykonywania testu.
Loren Pechtel

Nie zgadzam się co do czasu mieszania a czasu wyszukiwania. Podczas jednego przeszukiwania głowy możesz wykonać wiele obliczeń. Jeśli istnieje duże prawdopodobieństwo, że pliki pasują do siebie, użyłbym skrótu z wieloma bitami. Jeśli istnieje rozsądna szansa na dopasowanie, porównałbym je po jednym bloku, na przykład blokach 1 MB. (Wybierz rozmiar bloku, który dzieli równo 4k, aby mieć pewność, że nigdy nie podzielisz sektorów.)
Loren Pechtel,

1
Aby wyjaśnić wartość @ Guffa 99,999999998%, pochodzi ona z obliczeń 1 - (1 / (2^32)), co jest prawdopodobieństwem, że dowolny plik będzie miał 32-bitowy hash. Prawdopodobieństwo, że dwa różne pliki będą miały tę samą wartość skrótu jest takie samo, ponieważ pierwszy plik zawiera „podaną” wartość skrótu i ​​musimy tylko rozważyć, czy drugi plik pasuje do tej wartości. Szanse z 64- i 128-bitowym haszowaniem spadają do 99.999999999999999994% i 99.9999999999999999999999999999999999997% (odpowiednio), jakby to miało znaczenie przy tak niewyobrażalnych liczbach.
Glenn Slayden

... Rzeczywiście, fakt, że większości ludzi trudniej jest pojąć te liczby niż rzekomo proste, choć prawdziwe, pojęcie „nieskończenie wielu plików zderzających się z tym samym kodem skrótu”, może wyjaśniać, dlaczego ludzie są nieuzasadnieni podejrzliwi wobec akceptowania hash-as- równość.
Glenn Slayden,

13

Edycja: ta metoda nie zadziała przy porównywaniu plików binarnych!

W .NET 4.0 Fileklasa ma następujące dwie nowe metody:

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

Co oznacza, że ​​możesz użyć:

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

1
@dtb: Nie działa w przypadku plików binarnych. Prawdopodobnie już wpisałeś komentarz, kiedy zdałem sobie z tego sprawę i dodałem edycję na górze mojego postu. : o
Sam Harwell

@ 280Z28: Nic nie powiedziałem ;-)
dtb

Czy nie musiałbyś również przechowywać obu plików w pamięci?
RandomInsano

Zauważ, że File ma również funkcję ReadAllBytes, która może również używać SequenceEquals, więc użyj jej zamiast tego, ponieważ działałaby na wszystkich plikach. I jak powiedział @RandomInsano, jest to przechowywane w pamięci, więc chociaż jest perfekcyjnie w porządku w przypadku małych plików, uważałbym, używając go z dużymi plikami.
DaedalusAlpha

1
@DaedalusAlpha Zwraca wartość wyliczalną, więc wiersze będą ładowane na żądanie i nie będą przechowywane w pamięci przez cały czas. Z drugiej strony ReadAllBytes zwraca cały plik jako tablicę.
IllidanS4 chce, aby Monica wróciła

7

Szczerze mówiąc, myślę, że musisz przyciąć swoje drzewo wyszukiwania tak bardzo, jak to możliwe.

Rzeczy do sprawdzenia przed przejściem bajt po bajcie:

  1. Czy rozmiary są takie same?
  2. Czy ostatni bajt w pliku A różni się od pliku B

Również jednoczesne odczytywanie dużych bloków będzie bardziej wydajne, ponieważ dyski szybciej odczytują kolejne bajty. Przechodzenie bajt po bajcie powoduje nie tylko znacznie więcej wywołań systemowych, ale także powoduje, że głowica czytająca tradycyjnego dysku twardego częściej szuka tam i z powrotem, jeśli oba pliki znajdują się na tym samym dysku.

Odczytaj fragment A i fragment B do bufora bajtowego i porównaj je (NIE używaj Array.Equals, patrz komentarze). Dostosuj rozmiar bloków, aż osiągniesz to, co uważasz za dobrą kompromis między pamięcią a wydajnością. Możesz również wielowątkowość porównania, ale nie wielowątkowość odczytów dysku.


Używanie Array.Equals jest złym pomysłem, ponieważ porównuje całą tablicę. Jest prawdopodobne, że przynajmniej jeden odczyt bloku nie wypełni całej tablicy.
Doug Clutter

Dlaczego porównywanie całej tablicy to zły pomysł? Dlaczego odczyt bloku nie miałby wypełniać tablicy? Na pewno jest dobry punkt strojenia, ale dlatego bawisz się rozmiarami. Dodatkowe punkty za wykonanie porównania w osobnym wątku.
RandomInsano,

Kiedy definiujesz tablicę bajtów, będzie ona miała stałą długość. (np. - var buffer = new byte [4096]) Kiedy czytasz blok z pliku, może on zwrócić pełne 4096 bajtów lub nie. Na przykład, jeśli plik ma tylko 3000 bajtów długości.
Doug Clutter

Ach, teraz rozumiem! Dobra wiadomość jest taka, że ​​odczyt zwróci liczbę bajtów załadowanych do tablicy, więc jeśli tablicy nie można zapełnić, będą dane. Ponieważ testujemy pod kątem równości, stare dane bufora nie będą miały znaczenia. Dokumenty: msdn.microsoft.com/en-us/library/9kstw824(v=vs.110).aspx
RandomInsano

Co ważne, moje zalecenie użycia metody Equals () jest złym pomysłem. W Mono dokonują porównania pamięci, ponieważ elementy są ciągłe w pamięci. Microsoft jednak go nie zastępuje, zamiast tego wykonuje tylko porównanie referencyjne, które tutaj zawsze byłoby fałszywe.
RandomInsano

4

Moja odpowiedź jest pochodną @lars, ale naprawia błąd w wywołaniu Stream.Read. Dodam również szybkie sprawdzanie ścieżki, które miały inne odpowiedzi, i walidację danych wejściowych. W skrócie, to powinno być odpowiedź:

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

A jeśli chcesz być super-niesamowity, możesz użyć wariantu asynchronicznego:

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

czy bit konwertera bitów nie byłby lepszy jako `` for (var i = 0; i <count; i + = sizeof (long)) {if (BitConverter.ToInt64 (buffer1, i)! = BitConverter.ToInt64 (buffer2, i)) {return false; }} ``
Simon

2

Moje eksperymenty pokazują, że zdecydowanie pomaga wywołać Stream.ReadByte () mniej razy, ale użycie BitConverter do pakowania bajtów nie robi dużej różnicy w porównaniu z porównywaniem bajtów w tablicy bajtów.

Można więc zastąpić pętlę „Math.Ceiling and iterations” w powyższym komentarzu na najprostszą:

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

Wydaje mi się, że ma to związek z faktem, że BitConverter.ToInt64 musi wykonać trochę pracy (sprawdzić argumenty, a następnie wykonać przesunięcie bitowe) przed porównaniem, a to kończy się taką samą ilością pracy, jak porównanie 8 bajtów w dwóch tablicach .


1
Array.Equals wnika głębiej w system, więc prawdopodobnie będzie o wiele szybsze niż przechodzenie bajt po bajcie w C #. Nie mogę mówić w imieniu Microsoftu, ale w głębi duszy Mono używa polecenia memcpy () w języku C do równości tablic. Nie może być dużo szybciej.
RandomInsano

2
@RandomInsano zgadnij, że masz na myśli memcmp (), a nie memcpy ()
SQL Police

1

Jeśli pliki nie są zbyt duże, możesz użyć:

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

Porównywanie skrótów będzie możliwe tylko wtedy, gdy będą one przydatne do przechowywania.

(Zmodyfikowałem kod na coś znacznie bardziej przejrzystego).


1

Innym ulepszeniem w przypadku dużych plików o identycznej długości może być nie odczytywanie plików po kolei, a raczej porównywanie mniej lub bardziej losowych bloków.

Możesz używać wielu wątków, zaczynając od różnych pozycji w pliku i porównując do przodu lub do tyłu.

W ten sposób możesz wykryć zmiany w środku / na końcu pliku, szybciej niż w przypadku podejścia sekwencyjnego.


1
Czy awaria dysku spowodowałaby tutaj problemy?
RandomInsano

Fizyczne dyski twarde tak, dyski SSD sobie z tym poradzą.
TheLegendaryCopyCoder

1

Jeśli potrzebujesz tylko porównać dwa pliki, myślę, że najszybszym sposobem byłoby (w C nie wiem, czy ma to zastosowanie do .NET)

  1. otwórz oba pliki f1, f2
  2. pobierz odpowiednią długość pliku l1, l2
  3. jeśli l1! = l2, pliki są różne; zatrzymać
  4. mmap () oba pliki
  5. użyj memcmp () na plikach mmap () ed

OTOH, jeśli chcesz sprawdzić, czy w zestawie N plików znajdują się zduplikowane pliki, najszybszym sposobem jest niewątpliwie użycie skrótu, aby uniknąć N-stronnych porównań bit po bicie.


1

Coś (miejmy nadzieję) w miarę wydajnego:

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

Oto kilka funkcji narzędziowych, które pozwalają określić, czy dwa pliki (lub dwa strumienie) zawierają identyczne dane.

Udostępniłem „szybką” wersję, która jest wielowątkowa, ponieważ porównuje tablice bajtów (każdy bufor wypełniony z tego, co zostało odczytane w każdym pliku) w różnych wątkach przy użyciu zadań.

Zgodnie z oczekiwaniami jest znacznie szybszy (około 3x szybszy), ale zużywa więcej procesora (ponieważ jest wielowątkowy) i więcej pamięci (ponieważ potrzebuje dwóch bajtowych buforów tablicy na wątek porównania).

    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

Myślę, że są aplikacje, w których „mieszanie” jest szybsze niż porównywanie bajt po bajcie. Jeśli chcesz porównać plik z innymi lub mieć miniaturę zdjęcia, które można zmienić. To zależy od tego, gdzie i jak jest używany.

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

Tutaj możesz uzyskać to, co jest najszybsze.

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

Opcjonalnie możemy zapisać hash w bazie danych.

Mam nadzieję, że to pomoże


0

Jeszcze inna odpowiedź, pochodząca z @chsh. MD5 z zastosowaniami i skrótami do pliku to samo, plik nie istnieje i różne długości:

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

Mówisz, w if (i>=secondHash.Length ...jakich okolicznościach dwa skróty MD5 miałyby różne długości?
frogpelt

-1

To, jak odkryłem, działa dobrze, porównując najpierw długość bez czytania danych, a następnie porównując odczytaną sekwencję bajtów

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)));
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.