.NET CORE中比较两个文件内容是否相同的最快方法
前言
最近项目有个需求,需要比较两个任意大小文件的内容是否相同,要求如下:
- 项目是.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秒,感觉还不错.然而,这是最佳的解决方案吗?
其实我们仔细想一下,答案应该是否定的.
因为任何哈希算法本质上都是对字节进行一定的计算,而计算过程是要消耗时间的.
很多下载网站上提供了下载文件的哈希值,那是因为下载的源文件本身不会改变,只需要计算一次源文件的哈希值,提供给用户验证即可.
而我们的需求中,两个文件都是不固定的,那么每次都要计算两个文件的哈希值,就不太合适了.
所以,哈希比较这个方案被pass.
这种求算法最优解的问题,我以往的经验是: 去*查找 :)
经过我的艰苦努力,找到了一个非常切题的答案: how to compare 2 files fast using .net?
得赞最多一个答案,将代码改造了一下放入工程中:
/// <summary> /// https://*.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,再进行数值比较.那么效率如何呢?
method: comparebytoint64, identical: true. elapsed: 00:00:08.0918099
什么?8秒!竟然比md5还慢?这不是so得赞最多的答案吗,怎么会这样?
其实分析一下不难想到原因,因为每次只读取8个字节,程序频繁的进行io操作,导致性能低下.看来so上的答案也不能迷信啊!
那么优化的方向就变为了如何减少io操作带来的损耗.
既然每次8个字节太少了,我们定义一个大一些的字节数组,比如1024个字节.每次读取1024个字节到数组中,然后进行字节数组的比较.
但是这样又带来一个新问题,就是如何快速比较两个字节数组是否相同?
我首先想到的是在md5方法中用过的----将字节数组转换成字符串进行比较:
/// <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秒,比上一个方法强不了多少.
分析一下原因,在每次循环中,字符串的转换是一个非常耗时的操作.那么有没有不进行类型转换的字节数组比较方法呢?
我想到了linq中有一个比较序列的方法sequenceequal,我们尝试使用该方法比较:
/// <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循环比较字节数组怎么样呢?
/// <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秒左右,读者对于该方法是否满意呢?
no!我不满意!我相信通过努力,一定会找到更快的方法的!
同样.net core也在为了编写高性能代码而不断的优化中.
那么,我们如何继续优化我们的代码呢?
我突然想到在c# 7.2中加入的一个新的值类型: span<t>,它用来代表一段连续的内存区域,并提供一系列可操作该区域的方法.
对于我们的需求,因为我们不会更改数组的值,所以可以使用另外一个只读的类型readonlyspan<t>追求更高的效率.
修改代码,使用readonlyspan<t>:
/// <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中一样,实现完全不同.
那么该方法的表现如何呢?
method: comparebyreadonlyspan, identical: true. elapsed: 00:00:00.9287703
不 到 一 秒!
相对上一个已经不错的结果,速度提高了差不多40%!
对此结果,我个人觉得已经很满意了,如果各位有更快的方法,请不吝赐教,我非常欢迎!
关于span<t>结构类型,各位读者如有兴趣,可浏览该文章,该文有非常详细的介绍.
后记
文中的代码只是出于实验性质,实际应用中仍可以继续细节上的优化, 如:
- 如两个文件大小不同,直接返回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); } }
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对的支持。
下一篇: vs代码段快捷键设置(图文)