《.NET 下最快比较两个文件内容是否相同》之我的看法验证

我对文件对比这一块还是比较感兴趣的,也想知道哪种方式性价比最高,效率最好,所以,根据这篇文章,我自己也自测一下,顺便留出自己对比的结果,供大佬们参考一二。

大致对比方案

我这边根据文章里的主要三个方案.

  1. 1. MD5

  2. 2. 缓存长度读取比较

  3. 3. 缓存长度读取(Span)比较

  4. 4. Hash256

  5. 5. CRC(最后补的)

新增了Hash256模式,原因是因为GitHub就是用Hash256来确定文件的唯一性的,所以,也想测试下它的性能到底如何。

又新增了CRC模式,后来才想起来的。

代码大致如下

挺简单的一个抽象工具类,方便增加相关的相同测试。 我也搜了OpenAi的建议和NewBing的建议,大致都是一样的建议。

《.NET 下最快比较两个文件内容是否相同》之我的看法验证

根据建议和参考文章

抽象工具

public abstract class AFileCompare
{
    public AFileCompare(string name)
    {
        this.Name = name;
    }
    public string Name { get; set; }
    public bool Compare(string file1, string file2)
    {
        var result = false;
        Stopwatch stopwatch = Stopwatch.StartNew();
        if (Check(file1, file2))
        {
            result = CompareCore(file1, file2);
        }
        stopwatch.Stop();
        TimeSpan = stopwatch.Elapsed;
        return result;
    }
    public abstract bool CompareCore(string file1, string file2);
    public TimeSpan TimeSpan { get; set; }
    public bool Check(string file1, string file2)
    {
        if (file1 == file2)
        {
            return true;
        }
        if (new FileInfo(file1).Length == new FileInfo(file2).Length)
        {
            return true;
        }
        return false;
    }
    public override string ToString()
    {
        return $"{Name}耗时:{TimeSpan.TotalSeconds}秒";
    }
}

MD5工具

public class MD5Compare : AFileCompare
{
    public MD5Compare() : base("MD5 ")
    {
    }

    public override bool CompareCore(string file1, string file2)
    {
        using (var md5 = MD5.Create())
        {
            byte[] one, two;
            using (var fs1 = File.Open(file1, FileMode.Open))
            {
                // 以FileStream读取文件内容,计算HASH值
                one = md5.ComputeHash(fs1);
            }
            using (var fs2 = File.Open(file2, FileMode.Open))
            {
                // 以FileStream读取文件内容,计算HASH值
                two = md5.ComputeHash(fs2);
            }
            for (int i = 0; i < one.Length; i++)
            {
                if (one[i] != two[i])
                {
                    return false;
                }
            }
            return true;
        }
    }
}

Hash256

public class HashCompare : AFileCompare
{
    public HashCompare() : base("Hash256")
    {
    }

    public override bool CompareCore(string file1, string file2)
    {
        byte[] one, two;
        using (SHA1 mySHA1 = SHA1.Create())
        {
            using (FileStream stream = File.OpenRead(file1))
            {
                one = mySHA1.ComputeHash(stream);
            }
        }
        using (SHA1 mySHA1 = SHA1.Create())
        {
            using (FileStream stream = File.OpenRead(file2))
            {
                two = mySHA1.ComputeHash(stream);
            }
        }
        for (int i = 0; i < one.Length; i++)
        {
            if (one[i] != two[i])
            {
                return false;
            }
        }
        return true;
     }
}

缓存长度读取比较

public class FileSizeCompare : AFileCompare
{
    public FileSizeCompare() : base("FileSize_4096")
    {
    }

    public override bool CompareCore(string file1, string file2)
    {
        using (FileStream fs1 = new FileStream(file1, FileMode.Open))
        using (FileStream fs2 = new FileStream(file2, FileMode.Open))
        {
            byte[] buffer1 = new byte[4096];
            byte[] buffer2 = new byte[4096];

            int bytesRead1;
            int bytesRead2;

            do
            {
                bytesRead1 = fs1.Read(buffer1, 0, buffer1.Length);
                bytesRead2 = fs2.Read(buffer2, 0, buffer2.Length);

                if (bytesRead1 != bytesRead2)
                {
                    return false;
                }

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

            return true;
        }
    }
}

缓存长度读取(Span)比较

public class FileSizeCompare2 : AFileCompare
{
    public FileSizeCompare2() : base("FileSize_4096_Span")
    {
    }

    public override bool CompareCore(string file1, string file2)
    {
        using (FileStream fs1 = new FileStream(file1, FileMode.Open))
        using (FileStream fs2 = new FileStream(file2, FileMode.Open))
        {
            byte[] buffer1 = new byte[4096];
            byte[] buffer2 = new byte[4096];

            int bytesRead1;
            int bytesRead2;

            do
            {
                bytesRead1 = fs1.Read(buffer1, 0, buffer1.Length);
                bytesRead2 = fs2.Read(buffer2, 0, buffer2.Length);

                if (bytesRead1 != bytesRead2)
                {
                    return false;
                }

                if (!((ReadOnlySpan<byte>)buffer1).SequenceEqual((ReadOnlySpan<byte>)buffer2))
                {
                    return false;
                }
            } while (bytesRead1 > 0);

            return true;
        }
    }
}

CRC(补充)

public class CRCCompare : AFileCompare
{
    public CRCCompare() : base("CRC")
    {
    }
    public override bool CompareCore(string file1, string file2)
    {
        Crc32Algorithm crc32 = new Crc32Algorithm();
        byte[] one, two;
        using (var fs1 = File.Open(file1, FileMode.Open))
        {
            one = crc32.ComputeHash(fs1);
        }
        using (var fs2 = File.Open(file2, FileMode.Open))
        {
            two = crc32.ComputeHash(fs2);
        }
        for (int i = 0; i < one.Length; i++)
        {
            if (one[i] != two[i])
            {
                return false;
            }
        }
        return true;
    }
}

main 入口

static void Main(string[] args)
{
    var files = new List<(string name, string source, string target)>()
    {
        ("ubuntu_4.58 GB",@"E:\重装系统\ubuntu-22.04.2-desktop-amd64.iso",@"E:\重装系统\ubuntu-22.04.2-desktop-amd64 - 副本.iso"),
        ("Docker_523 MB",@"E:\重装系统\Docker Desktop Installer(1).exe",@"E:\重装系统\Docker Desktop Installer(1) - 副本.exe"),
        ("Postman_116 MB",@"E:\重装系统\Postman-win64-8.5.0-Setup.exe",@"E:\重装系统\Postman-win64-8.5.0-Setup - 副本.exe")
    };
    var list = new List<AFileCompare>();
    list.Add(new MD5Compare());
    list.Add(new HashCompare());
    list.Add(new FileSizeCompare());
    list.Add(new FileSizeCompare2());
    foreach ((string name, string source, string target) in files)
    {
        foreach (var item in list)
        {
            var result = item.Compare(source, target);
            Console.WriteLine($"{name} - {item.Name} 结果:{result} {item}");
        }
        Console.WriteLine();
    }
    Console.WriteLine("测试完毕");
    Console.ReadLine();
}

测试结果如下

主要是对3个文件,4.5G,500M,100M文件进行了测试,大概也能看出来点结果了。

测试1

《.NET 下最快比较两个文件内容是否相同》之我的看法验证

测试2

《.NET 下最快比较两个文件内容是否相同》之我的看法验证

测试3

《.NET 下最快比较两个文件内容是否相同》之我的看法验证

CRC补充三次

第一次

《.NET 下最快比较两个文件内容是否相同》之我的看法验证

第二次

《.NET 下最快比较两个文件内容是否相同》之我的看法验证

第三次

《.NET 下最快比较两个文件内容是否相同》之我的看法验证

结果对比

《.NET 下最快比较两个文件内容是否相同》之我的看法验证

根据我分析的表大致可以看出来,五种方式,根据各种不同的文件对比方式,在文件大小不一样的情况下,差别还是挺大的,特别是在大文件的情况下。

文件越大,反而MD5和Hash效果会更好。 文件小于1G ?,FileSize_Span方式会更优,除了MD5,其他方式基本都差不多,CRC属于不上也不下。

虽然,环境的因素会有一定的影响,假设我这个就是客户的机器,那可能性也是存在的。

所以,在同一台机器上的多次测试也有存在的合理性。

那么,如果对性能很在意的话,可以根据文件大小与各种对比方法之间的关系,找到一个函数映射关系,从而找到一个最优的对比方法出来。

总结

还挺意外的,本来想验证这个Span方式为何快,却看到了另外问题点,出乎意料。

另外,也看到了Github采用Hash256在获取指纹上,速度还是相对稳定的。那么,用它来对比文件还是提取指纹应该是最佳之选(性能要求苛刻的,得自己搞模型求最佳函数映射关系)

还有其他的细节优化还没有验证,但是Span的方式是值得借鉴和学习的。

《.NET 下最快比较两个文件内容是否相同》参考地址

https://www.cnblogs.com/waku/p/11069214.html

代码地址

https://github.com/kesshei/FileCompare.git

https://gitee.com/kesshei/FileCompare.git