วิธีเปรียบเทียบ 2 ไฟล์อย่างรวดเร็วโดยใช้. NET


137

วิธีการทั่วไปแนะนำให้อ่านไบนารีผ่าน FileStream และเปรียบเทียบแบบไบต์ต่อไบต์

  • การเปรียบเทียบ checksum เช่น CRC จะเร็วกว่าหรือไม่?
  • มีไลบรารี. NET ที่สามารถสร้างการตรวจสอบสำหรับไฟล์หรือไม่

คำตอบ:


118

การเปรียบเทียบเช็คซัมมักจะช้ากว่าการเปรียบเทียบแบบไบต์ต่อไบต์

ในการสร้างการตรวจสอบคุณจะต้องโหลดไฟล์แต่ละไบต์และดำเนินการประมวลผล จากนั้นต้องทำในไฟล์ที่สอง การประมวลผลเกือบจะช้ากว่าการตรวจสอบเปรียบเทียบ

สำหรับการสร้างการตรวจสอบ: คุณสามารถทำได้อย่างง่ายดายด้วยคลาสการเข้ารหัส นี่คือตัวอย่างสั้น ๆ ของการสร้างการตรวจสอบ MD5ด้วย C #

อย่างไรก็ตามการตรวจสอบอาจเร็วกว่าและเหมาะสมกว่าหากคุณสามารถคำนวณผลการตรวจสอบของกรณี "ทดสอบ" หรือ "ฐาน" ล่วงหน้าได้ หากคุณมีไฟล์ที่มีอยู่และคุณกำลังตรวจสอบเพื่อดูว่าไฟล์ใหม่เหมือนกับไฟล์ที่มีอยู่หรือไม่การคำนวณเช็คซัมบนไฟล์ "ที่มีอยู่" ของคุณล่วงหน้าหมายความว่าต้องทำ DiskIO เพียงครั้งเดียวใน ไฟล์ใหม่ สิ่งนี้น่าจะเร็วกว่าการเปรียบเทียบแบบไบต์ต่อไบต์


30
อย่าลืมคำนึงถึงตำแหน่งที่ไฟล์ของคุณอยู่ หากคุณกำลังเปรียบเทียบไฟล์ในเครื่องกับการสำรองข้อมูลครึ่งทางทั่วโลก (หรือผ่านเครือข่ายที่มีแบนด์วิดท์ที่น่ากลัว) คุณอาจควรแฮชก่อนและส่งการตรวจสอบผ่านเครือข่ายแทนที่จะส่งสตรีมไบต์ไปยัง เปรียบเทียบ.
คิม

@ReedCopsey: ฉันมีปัญหาที่คล้ายกันเนื่องจากฉันต้องการจัดเก็บไฟล์อินพุต / เอาท์พุตที่เกิดจากการทำอย่างละเอียดหลายอย่างซึ่งควรจะมีการทำซ้ำจำนวนมาก ฉันคิดว่าจะใช้แฮชที่คำนวณไว้ล่วงหน้า แต่คุณคิดว่าฉันสามารถสันนิษฐานได้อย่างสมเหตุสมผลว่าหากแฮช 2 (เช่น MD5) เท่ากัน 2 ไฟล์จะเท่ากันและหลีกเลี่ยงการเปรียบเทียบไบต์ -2 ไบต์เพิ่มเติม เท่าที่ฉันรู้ว่าการชนกันของ MD5 / SHA1 และอื่น ๆ นั้นไม่น่าเป็นไปได้จริงๆ ...
digEmAl

1
@digEmA โอกาสในการชนกันจะต่ำ - คุณสามารถแฮชที่แรงกว่าได้เสมอเช่นใช้ SHA256 แทน SHA1 ซึ่งจะช่วยลดโอกาสในการชนต่อไป
Reed Copsey

ขอบคุณสำหรับคำตอบ - ฉันเพิ่งเข้าสู่. net ฉันสมมติว่าถ้ามีใครใช้เทคนิค hashcode / check sum แฮชของโฟลเดอร์หลักจะถูกเก็บไว้อย่างต่อเนื่องที่ไหนสักแห่ง? ด้วยความอยากรู้อยากเห็นว่าคุณจะจัดเก็บไว้สำหรับแอปพลิเคชัน 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 เฉลี่ยมากกว่า 1,000 รันฉันได้วิธีนี้ที่ 1,063ms และวิธีการด้านล่าง (การเปรียบเทียบไบต์แบบตรงไปตรงมาโดยไบต์) ที่ 3031ms การแฮชมักจะกลับมาเป็นวินาทีย่อยที่ค่าเฉลี่ย 865 มิลลิวินาที การทดสอบนี้ใช้ไฟล์วิดีโอ ~ 100MB

นี่คือ ReadByte และวิธีการแฮชที่ฉันใช้เพื่อการเปรียบเทียบ:

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

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

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

        return true;
    }

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

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

1
คุณทำให้ชีวิตของฉันง่ายขึ้น ขอบคุณ
anindis

2
@anindis: เพื่อความสมบูรณ์คุณอาจต้องการที่จะอ่านทั้งคำตอบ @Lars'และ@ คำตอบของ ดีใจที่ช่วยได้หลายปี! :)
chsh

1
FilesAreEqual_Hashวิธีการควรจะมีusingในไฟล์ทั้งลำธารเกินไปเช่นReadByteวิธีการมิฉะนั้นจะแขวนในทั้งสองไฟล์
Ian Mercer

2
โปรดทราบว่าFileStream.Read()อาจอ่านไบต์น้อยกว่าตัวเลขที่ร้องขอ คุณควรใช้StreamReader.ReadBlock()แทน
Palec

2
ในเวอร์ชัน Int64 เมื่อความยาวของสตรีมไม่ใช่จำนวนทวีคูณของ Int64 การวนซ้ำครั้งสุดท้ายจะเปรียบเทียบไบต์ที่ไม่ได้เติมโดยใช้การเติมซ้ำก่อนหน้านี้ (ซึ่งควรจะเท่ากันด้วยจึงจะดี) นอกจากนี้หากความยาวของสตรีมน้อยกว่า sizeof (Int64) ไบต์ที่ไม่ได้เติมจะเป็น 0 เนื่องจาก C # เริ่มต้นอาร์เรย์ IMO โค้ดน่าจะแสดงความแปลกประหลาดเหล่านี้
crokusek

52

หากคุณไม่ตัดสินใจว่าคุณอย่างแท้จริงต้องมีการเปรียบเทียบไบต์โดยไบต์เต็มรูปแบบ (ดูคำตอบอื่น ๆ สำหรับการอภิปรายของคร่ำเครียด) แล้วทางออกที่ง่ายที่สุดคือ:


•สำหรับชื่อพา ธ "System.String`:
public static bool AreFileContentsEqual(String path1, String path2) =>
              File.ReadAllBytes(path1).SequenceEqual(File.ReadAllBytes(path2));

•สำหรับอินสแตนซ์ "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)));

ไม่เหมือนบางคำตอบที่โพสต์อื่น ๆ นี้เป็นสรุปที่ถูกต้องสำหรับชนิดของไฟล์ใด ๆ :ไบนารี, ข้อความ, สื่อปฏิบัติการ ฯลฯ แต่เป็นเต็มรูปแบบไบนารีเปรียบเทียบไฟล์ที่แตกต่างกันเพียงในรูปแบบ "ไม่สำคัญ" (เช่นBOM , สาย -ending , การเข้ารหัสอักขระเมสื่อ, ความเห็นช่องว่างช่องว่างภายในซอร์สโค้ด ฯลฯ ) มักจะได้รับการพิจารณาที่ไม่เท่ากัน

รหัสนี้จะโหลดไฟล์ทั้งในหน่วยความจำอย่างสิ้นเชิงดังนั้นจึงควรไม่สามารถใช้สำหรับการเปรียบเทียบขนาดยักษ์อย่างแท้จริงไฟล์ นอกเหนือจากข้อแม้ที่สำคัญแล้วการโหลดแบบเต็มไม่ใช่บทลงโทษจากการออกแบบ. NET GC (เนื่องจากได้รับการปรับให้เหมาะสมโดยพื้นฐานเพื่อให้การจัดสรรที่มีขนาดเล็กและมีอายุการใช้งานสั้นราคาถูกมาก ) และในความเป็นจริงอาจเหมาะสมที่สุดเมื่อคาดว่าจะมีขนาดไฟล์ จะน้อยกว่า85Kเพราะใช้ต่ำสุดของรหัสผู้ใช้ (ดังแสดงที่นี่) หมายถึงการมอบหมายที่สุดปัญหาประสิทธิภาพการทำงานไฟล์ไปCLR, BCLและJITที่จะได้รับประโยชน์จาก (เช่น) เทคโนโลยีใหม่ล่าสุดที่ออกแบบรหัสระบบและการเพิ่มประสิทธิภาพรันไทม์การปรับตัว

นอกจากนี้สำหรับสถานการณ์การทำงานในวันทำงานเช่นนี้ความกังวลเกี่ยวกับประสิทธิภาพของการเปรียบเทียบแบบไบต์ต่อไบต์ผ่านตัวLINQนับ (ดังที่แสดงไว้ที่นี่) เป็นสิ่งที่น่าสงสัยเนื่องจากการกดปุ่มa̲t̲ a̲l̲l̲สำหรับไฟล์ I / O จะทำให้แคระแกร็นตามลำดับความสำคัญหลายขนาดประโยชน์ ของทางเลือกในการเปรียบเทียบหน่วยความจำต่างๆ ยกตัวอย่างเช่นแม้ว่าSequenceEqual ไม่ในความเป็นจริงให้เรา "การเพิ่มประสิทธิภาพ" ของทิ้งในไม่ตรงกันแรกนี้แทบจะไม่สำคัญหลังจากที่มีการเรียกแล้วเนื้อหาไฟล์แต่ละจำเป็นอย่างเต็มที่เพื่อยืนยันการแข่งขัน ..


3
ไฟล์นี้ดูไม่ดีสำหรับไฟล์ขนาดใหญ่ ไม่ดีสำหรับการใช้งานหน่วยความจำเนื่องจากจะอ่านทั้งสองไฟล์จนจบก่อนที่จะเริ่มเปรียบเทียบอาร์เรย์ไบต์ นั่นคือเหตุผลที่ฉันค่อนข้างจะเลือกใช้ streamreader ที่มีบัฟเฟอร์
Krypto_47

3
@ Krypto_47 ฉันพูดถึงปัจจัยเหล่านี้และการใช้ประโยชน์ในข้อความคำตอบของฉัน
Glenn Slayden

33

นอกจากคำตอบของReed Copsey แล้ว :

  • กรณีที่แย่ที่สุดคือไฟล์ทั้งสองไฟล์เหมือนกัน ในกรณีนี้ควรเปรียบเทียบไฟล์แบบไบต์ต่อไบต์

  • หากไฟล์ทั้งสองไฟล์ไม่เหมือนกันคุณสามารถเร่งความเร็วขึ้นเล็กน้อยโดยตรวจพบเร็วกว่าว่าไฟล์ไม่เหมือนกัน

ตัวอย่างเช่นหากไฟล์ทั้งสองมีความยาวต่างกันคุณจะรู้ว่าไฟล์นั้นไม่สามารถเหมือนกันได้และคุณไม่จำเป็นต้องเปรียบเทียบเนื้อหาจริงของไฟล์นั้นด้วยซ้ำ


10
เพื่อให้เสร็จสมบูรณ์: การเพิ่มขนาดใหญ่อื่น ๆ จะหยุดลงทันทีที่ไบต์ที่ 1 ตำแหน่งต่างกัน
Henk Holterman

6
@Henk: ฉันคิดว่ามันชัดเจนเกินไป :-)
dtb

1
จุดที่ดีในการเพิ่มสิ่งนี้ เห็นได้ชัดสำหรับฉันดังนั้นฉันจึงไม่ได้รวมไว้ แต่เป็นการดีที่จะพูดถึง
Reed Copsey

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)
Jack A.

14

สิ่งเดียวที่อาจทำให้การเปรียบเทียบ checksum เร็วกว่าการเปรียบเทียบแบบไบต์ต่อไบต์เล็กน้อยคือการที่คุณกำลังอ่านทีละไฟล์ซึ่งจะช่วยลดเวลาในการค้นหาสำหรับหัวดิสก์ได้บ้าง การเพิ่มขึ้นเล็กน้อยนั้นอาจถูกกินได้เป็นอย่างดีตามเวลาที่เพิ่มขึ้นของการคำนวณแฮช

นอกจากนี้การเปรียบเทียบเช็คซัมมีโอกาสที่จะเร็วขึ้นหากไฟล์เหมือนกัน หากไม่เป็นเช่นนั้นการเปรียบเทียบแบบไบต์ต่อไบต์จะสิ้นสุดที่ความแตกต่างแรกทำให้เร็วขึ้นมาก

นอกจากนี้คุณควรพิจารณาด้วยว่าการเปรียบเทียบรหัสแฮชจะบอกคุณเพียงว่ามีโอกาสมากที่ไฟล์จะเหมือนกัน เพื่อให้แน่ใจ 100% คุณต้องทำการเปรียบเทียบแบบไบต์ต่อไบต์

หากรหัสแฮชเป็น 32 บิตคุณจะแน่ใจได้ประมาณ 99.99999998% ว่าไฟล์นั้นเหมือนกันหากรหัสแฮชตรงกัน นั่นใกล้เคียง 100% แต่ถ้าคุณต้องการความมั่นใจ 100% อย่างแท้จริงนั่นไม่ใช่เลย


ใช้แฮชที่ใหญ่ขึ้นและคุณจะได้รับอัตราต่อรองของผลบวกเท็จจนถึงต่ำกว่าอัตราต่อรองที่คอมพิวเตอร์ทำผิดขณะทำการทดสอบ
Loren Pechtel

ฉันไม่เห็นด้วยเกี่ยวกับเวลาแฮชกับเวลาแสวงหา คุณสามารถทำจำนวนมากของการคำนวณในช่วงหัวเดียวแสวงหา หากอัตราต่อรองสูงที่ไฟล์ตรงกันฉันจะใช้แฮชที่มีบิตจำนวนมาก หากมีโอกาสที่เหมาะสมในการจับคู่ฉันจะเปรียบเทียบทีละบล็อกเช่นบล็อก 1MB (เลือกขนาดบล็อกที่แบ่ง 4k เท่า ๆ กันเพื่อให้แน่ใจว่าคุณจะไม่มีการแบ่งส่วน)
Loren Pechtel

1
เพื่ออธิบายตัวเลขของ @ Guffa 99.99999998% มันมาจากการคำนวณ1 - (1 / (2^32))ซึ่งเป็นความน่าจะเป็นที่ไฟล์ใดไฟล์หนึ่งจะมีแฮช 32 บิตที่กำหนด ความน่าจะเป็นของไฟล์ที่แตกต่างกันสองไฟล์ที่มีแฮชเดียวกันจะเหมือนกันเนื่องจากไฟล์แรกให้ค่าแฮช "ที่กำหนด" และเราจำเป็นต้องพิจารณาว่าไฟล์อื่นตรงกับค่านั้นหรือไม่ โอกาสในการแฮช 64- และ 128 บิตลดลงเหลือ 99.9999999999999994% และ 99.9999999999999999999999999999999999997% (ตามลำดับ) ราวกับว่ามีความสำคัญกับตัวเลขที่เข้าใจไม่ได้
Glenn Slayden

... อันที่จริงความจริงที่ว่าตัวเลขเหล่านี้ยากสำหรับคนส่วนใหญ่ที่จะเข้าใจมากกว่าแนวคิดที่เรียบง่ายแม้ว่าจะเป็นความจริงของ "ไฟล์จำนวนมากที่ชนกันเป็นรหัสแฮชเดียวกันโดยไม่มีเหตุผล " อาจอธิบายได้ว่าทำไมมนุษย์จึงสงสัยอย่างไม่มีเหตุผลที่จะยอมรับแฮช-as- ความเท่าเทียมกัน
Glenn Slayden

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
Sam Harwell

@ 280Z28: ฉันไม่ได้พูดอะไรเลย ;-)
dtb

คุณไม่จำเป็นต้องจัดเก็บทั้งสองไฟล์ไว้ในหน่วยความจำหรือ?
RandomInsano

โปรดทราบว่า File ยังมีฟังก์ชั่น ReadAllBytes ซึ่งสามารถใช้ SequenceEquals ได้เช่นกันดังนั้นให้ใช้สิ่งนั้นแทนเนื่องจากจะใช้ได้กับทุกไฟล์ และตามที่ @RandomInsano กล่าวไว้สิ่งนี้จะถูกเก็บไว้ในหน่วยความจำดังนั้นในขณะที่ใช้กับไฟล์ขนาดเล็กได้ดีฉันควรระมัดระวังในการใช้ไฟล์ขนาดใหญ่
DaedalusAlpha

1
@DaedalusAlpha ส่งกลับค่าที่แจกแจงได้ดังนั้นบรรทัดจะถูกโหลดตามความต้องการและไม่ได้เก็บไว้ในหน่วยความจำตลอดเวลา ในทางกลับกัน ReadAllBytes จะส่งคืนไฟล์ทั้งหมดเป็นอาร์เรย์
IllidanS4 รองรับ Monica

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

ที่สำคัญคำแนะนำของฉันให้ใช้วิธี Equals () เป็นความคิดที่ไม่ดี ในโมโนพวกเขาทำการเปรียบเทียบหน่วยความจำเนื่องจากองค์ประกอบอยู่ติดกันในหน่วยความจำ อย่างไรก็ตาม 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 (long)) {if (BitConverter ToInt64 (buffer1, i)! = BitConverter ToInt64 (buffer2, i)) {กลับเท็จ; }} ``
Simon

2

การทดลองของฉันแสดงให้เห็นว่าการเรียกใช้ Stream.ReadByte () จำนวนครั้งน้อยลงอย่างแน่นอน แต่การใช้ BitConverter เพื่อบรรจุไบต์ไม่ได้สร้างความแตกต่างมากนักเมื่อเทียบกับการเปรียบเทียบไบต์ในอาร์เรย์ไบต์

ดังนั้นจึงเป็นไปได้ที่จะแทนที่ "Math.Ceiling and การวนซ้ำ" ในความคิดเห็นด้านบนด้วยอันที่ง่ายที่สุด:

            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

OTOH หากคุณต้องการค้นหาว่ามีไฟล์ที่ซ้ำกันในชุดของไฟล์ N หรือไม่วิธีที่เร็วที่สุดคือการใช้แฮชเพื่อหลีกเลี่ยงการเปรียบเทียบ N-way bit-by-bit อย่างไม่ต้องสงสัย


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

ต่อไปนี้เป็นฟังก์ชันยูทิลิตี้บางอย่างที่ช่วยให้คุณตรวจสอบได้ว่าไฟล์สองไฟล์ (หรือสองสตรีม) มีข้อมูลที่เหมือนกันหรือไม่

ฉันได้จัดเตรียมเวอร์ชัน "เร็ว" ที่เป็นแบบมัลติเธรดเนื่องจากเปรียบเทียบอาร์เรย์ไบต์ (แต่ละบัฟเฟอร์เติมจากสิ่งที่อ่านในแต่ละไฟล์) ในเธรดที่แตกต่างกันโดยใช้ Tasks

ตามที่คาดไว้มันเร็วกว่ามาก (เร็วขึ้นประมาณ 3 เท่า) แต่ใช้ CPU มากขึ้น (เพราะเป็นแบบมัลติเธรด) และหน่วยความจำมากขึ้น (เนื่องจากต้องการบัฟเฟอร์อาร์เรย์สองไบต์ต่อเธรดการเปรียบเทียบ)

    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

สิ่งนี้ฉันพบว่าใช้งานได้ดีเมื่อเปรียบเทียบกับความยาวก่อนโดยไม่ต้องอ่านข้อมูลจากนั้นเปรียบเทียบลำดับการอ่านไบต์

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

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
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.