前言
最近项目有个需求,需要比较两个任意大小文件的内容是否相同,要求如下:
- 项目是.NET CORE,所以使用C#进行编写比较方法
- 文件大小任意,所以不能将文件内容全部读入到内存中进行比较(更专业点说,需要使用非缓存的比较方式)
- 不依赖第三方库
- 越快越好
为了选出最优的解决方案,我搭建了一个简单的命令行工程,准备了两个大小为912MB的文件,并且这两个文件内容完全相同.在本文的最后,你可以看到该工程的Main方法的代码.
下面我们开始尝试各个比较方法,选出最优的解决方案:
比较两个文件是否完全相同,首先想到的是用哈希算法(如MD5,SHA)算出两个文件的哈希值,然后进行比较.
废话少说,撸起袖子写一个MD5比较方法:
/// <summary> /// MD5 /// </summary> /// <param name="file1"></param> /// <param name="file2"></param> /// <returns></returns> private static bool CompareByMD5(string file1, string file2) { // 使用.NET内置的MD5库 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); } // 将MD5结果(字节数组)转换成字符串进行比较 return BitConverter.ToString(one) == BitConverter.ToString(two); } }
比较结果:
Method: CompareByMD5, Identical: True. Elapsed: 00:00:05.7933178
耗时5.79秒,感觉还不错.然而,这是最佳的解决方案吗"external nofollow" target="_blank" href="https://stackoverflow.com/questions/1358510/how-to-compare-2-files-fast-using-net/1359947#1359947">How to compare 2 files fast using .NET"htmlcode">
/// <summary> /// https://stackoverflow.com/a/1359947 /// </summary> /// <param name="file1"></param> /// <param name="file2"></param> /// <returns></returns> private static bool CompareByToInt64(string file1, string file2) { const int BYTES_TO_READ = sizeof(Int64); // 每次读取8个字节 int iterations = (int)Math.Ceiling((double)new FileInfo(file1).Length / BYTES_TO_READ); // 计算读取次数 using (FileStream fs1 = File.Open(file1, FileMode.Open)) using (FileStream fs2 = File.Open(file2, FileMode.Open)) { 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); // 转换为Int64进行数值比较 if (BitConverter.ToInt64(one, 0) != BitConverter.ToInt64(two, 0)) return false; } } return true; }
该方法基本的原理是循环读取两个文件,每次读取8个字节,转换为Int64,再进行数值比较.那么效率如何呢"htmlcode">
/// <summary> /// 读入到字节数组中比较(转为String比较) /// </summary> /// <param name="file1"></param> /// <param name="file2"></param> /// <returns></returns> private static bool CompareByString(string file1, string file2) { const int BYTES_TO_READ = 1024 * 10; using (FileStream fs1 = File.Open(file1, FileMode.Open)) using (FileStream fs2 = File.Open(file2, FileMode.Open)) { byte[] one = new byte[BYTES_TO_READ]; byte[] two = new byte[BYTES_TO_READ]; while (true) { int len1 = fs1.Read(one, 0, BYTES_TO_READ); int len2 = fs2.Read(two, 0, BYTES_TO_READ); if (BitConverter.ToString(one) != BitConverter.ToString(two)) return false; if (len1 == 0 || len2 == 0) break; // 两个文件都读取到了末尾,退出while循环 } } return true; }
结果:
Method: CompareByString, Identical: True. Elapsed: 00:00:07.8088732
耗时也接近8秒,比上一个方法强不了多少.
分析一下原因,在每次循环中,字符串的转换是一个非常耗时的操作.那么有没有不进行类型转换的字节数组比较方法呢"htmlcode">
/// <summary> /// 读入到字节数组中比较(使用LINQ的SequenceEqual比较) /// </summary> /// <param name="file1"></param> /// <param name="file2"></param> /// <returns></returns> private static bool CompareBySequenceEqual(string file1, string file2) { const int BYTES_TO_READ = 1024 * 10; using (FileStream fs1 = File.Open(file1, FileMode.Open)) using (FileStream fs2 = File.Open(file2, FileMode.Open)) { byte[] one = new byte[BYTES_TO_READ]; byte[] two = new byte[BYTES_TO_READ]; while (true) { int len1 = fs1.Read(one, 0, BYTES_TO_READ); int len2 = fs2.Read(two, 0, BYTES_TO_READ); if (!one.SequenceEqual(two)) return false; if (len1 == 0 || len2 == 0) break; // 两个文件都读取到了末尾,退出while循环 } } return true; }
结果:
Method: CompareBySequenceEqual, Identical: True. Elapsed: 00:00:08.2174360
竟然比前两个都要慢(实际这也是所有方案中最慢的一个),LINQ的SequenceEqual看来不是为了效率而生.
那么我们不用那些花哨的功能,回归质朴,老实儿的使用while循环比较字节数组怎么样呢"htmlcode">
/// <summary> /// 读入到字节数组中比较(while循环比较字节数组) /// </summary> /// <param name="file1"></param> /// <param name="file2"></param> /// <returns></returns> private static bool CompareByByteArry(string file1, string file2) { const int BYTES_TO_READ = 1024 * 10; using (FileStream fs1 = File.Open(file1, FileMode.Open)) using (FileStream fs2 = File.Open(file2, FileMode.Open)) { byte[] one = new byte[BYTES_TO_READ]; byte[] two = new byte[BYTES_TO_READ]; while (true) { int len1 = fs1.Read(one, 0, BYTES_TO_READ); int len2 = fs2.Read(two, 0, BYTES_TO_READ); int index = 0; while (index < len1 && index < len2) { if (one[index] != two[index]) return false; index++; } if (len1 == 0 || len2 == 0) break; } } return true; }
结果是....
Method: CompareByByteArry, Identical: True. Elapsed: 00:00:01.5356821
1.53秒!大突破!看来有时候看起来笨拙的方法反而效果更好!
试验到此,比较两个900多MB的文件耗时1.5秒左右,读者对于该方法是否满意呢"htmlcode">
/// <summary> /// 读入到字节数组中比较(ReadOnlySpan) /// </summary> /// <param name="file1"></param> /// <param name="file2"></param> /// <returns></returns> private static bool CompareByReadOnlySpan(string file1, string file2) { const int BYTES_TO_READ = 1024 * 10; using (FileStream fs1 = File.Open(file1, FileMode.Open)) using (FileStream fs2 = File.Open(file2, FileMode.Open)) { byte[] one = new byte[BYTES_TO_READ]; byte[] two = new byte[BYTES_TO_READ]; while (true) { int len1 = fs1.Read(one, 0, BYTES_TO_READ); int len2 = fs2.Read(two, 0, BYTES_TO_READ); // 字节数组可直接转换为ReadOnlySpan if (!((ReadOnlySpan<byte>)one).SequenceEqual((ReadOnlySpan<byte>)two)) return false; if (len1 == 0 || len2 == 0) break; // 两个文件都读取到了末尾,退出while循环 } } return true; }
核心是用来比较的SequenceEqual方法,该方法是ReadOnlySpan的一个扩展方法,要注意它只是方法名与LINQ中一样,实现完全不同.
那么该方法的表现如何呢"color: #ff0000">后记
文中的代码只是出于实验性质,实际应用中仍可以继续细节上的优化, 如:
- 如两个文件大小不同,直接返回false
- 如果两个文件路径相同,直接返回true
- ...
试验工程的Main方法源码:
static void Main(string[] args) { string file1 = @"C:\Users\WAKU\Desktop\file1.ISO"; string file2 = @"C:\Users\WAKU\Desktop\file2.ISO"; var methods = new Func<string, string, bool>[] { CompareByMD5, CompareByToInt64, CompareByByteArry, CompareByReadOnlySpan }; foreach (var method in methods) { var sw = Stopwatch.StartNew(); bool identical = method(file1, file2); Console.WriteLine("Method: {0}, Identical: {1}. Elapsed: {2}", method.Method.Name, identical, sw.Elapsed); } }
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对的支持。