.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.

这种求算法最优解的问题,我以往的经验是: 去stackoverflow查找 :)

经过我的艰苦努力,找到了一个非常切题的答案: How to compare 2 files fast using .NET?

得赞最多一个答案,将代码改造了一下放入工程中:

/// <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,再进行数值比较.那么效率如何呢?

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

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对我们的支持。

(0)

相关推荐

  • .NET CORE中比较两个文件内容是否相同的最快方法

    前言 最近项目有个需求,需要比较两个任意大小文件的内容是否相同,要求如下: 项目是.NET CORE,所以使用C#进行编写比较方法 文件大小任意,所以不能将文件内容全部读入到内存中进行比较(更专业点说,需要使用非缓存的比较方式) 不依赖第三方库 越快越好 为了选出最优的解决方案,我搭建了一个简单的命令行工程,准备了两个大小为912MB的文件,并且这两个文件内容完全相同.在本文的最后,你可以看到该工程的Main方法的代码. 下面我们开始尝试各个比较方法,选出最优的解决方案: 比较两个文件是否完全相

  • ASP.NET Core中预压缩静态文件的方法步骤

    前言 Web应用程序的优化是非常重要,因为使用更少的CPU,占用更少的带宽可以减少项目的费用. 在ASP.NET Core中我们可以很容易的启用响应压缩,但是针对预压缩文件,就需要做一些额外的功能了. 这篇博客文章展示了如何在ASP.NET Core中预压缩静态文件. 下面话不多说了,来一起看看详细的介绍吧 为什么需要预压缩文件? 虽然在从服务器请求文件时, 我们可以动态压缩文件,但这意味这Web服务器需要做更多的额外工作. 其实只有在新的应用程序部署时才会更改要压缩的文件. 越好的压缩效果需要

  • AngularJS中update两次出现$promise属性无法识别的解决方法

    前言 本文主要介绍的是在AngularJS中update两次出现$promise属性无法识别的解决方法,下面话不多说,先来看看错误提示,然后再看看解决的办法吧. 一.错误信息如下: ERROR 2015-12-02 14:33:17,653 http-bio-8080-exec-42 o.s.s.r.i.e.InternalErrorExceptionMapper - Unrecognized field "$promise" (class com.inetpsa.fnd.rest.c

  • 在html中引入外部js文件,并调用带参函数的方法

    在html中引入外部js文件,并调用js文件中的带参函数 1 项目结构 2 编写a.js.test.html //a.js function abc(str){ alert(str); } //test.html <html> <head> <script type="text/javascript" src="a.js"></script> <script type="text/javascript&

  • JS中的两种数据类型及实现引用类型的深拷贝的方法

    一.前言 我们知道,在JS中数据类型按照访问方式和存储方式的不同可分为基本类型和引用类型. 基本类型 基本类型有String.Boolean.Number,Undefined.Null,这些基本类型都是按值传递的,也称为值类型. 引用类型 引用类型有对象.数组.函数,它们都是按引用访问的. 二.存储方式区别 基本类型和引用类型由于两者在内存中存储的方式不同,造成两者访问的方式也不同.其中,基本类型存储在内存的栈中,是按值访问:引用类型存储在内存的堆中,是按引用访问.可如下图所示: 当有 var

  • C++ 读取文件内容到指定类型的变量方法

    如下所示: #include <iostream> #include <fstream> #include <sstream> #include <string> using namespace std; int main(){ cout << "input the file name: "; string file_name; cin >> file_name; cout << endl; // if

  • C# 如何判断两个文件内容是否相同的方法

    该哈希算法为一个文件生成一个小的二进制"指纹",从统计学的角度来看,不同的文件不可能生成相同的哈希码 要生成一个哈希码,必须首先创建一个HashAlgorithm对象,通过HashAlgorithm.Create方法来完成.然后调用 HashAlgorithm.ComputeHash方法,它会返回一个存储哈希码的字节数组,再使用BitConverter.Tostring()将其 装换为字符串进行比较. 源码如下: 复制代码 代码如下: public static bool isVali

  • 如何在Asp.Net Core中集成Refit

    在很多时候我们在不同的服务之间需要通过HttpClient进行及时通讯,在我们的代码中我们会创建自己的HttpClient对象然后去跨领域额进行数据的交互,但是往往由于一个项目有多个人开发所以在开发中没有人经常会因为不同的业务请求去写不同的代码,然后就会造成各种风格的HttpClient的跨域请求,最重要的是由于每个人对HttpClient的理解程度不同所以写出来的代码可能质量上会有参差不齐,即使代码能够达到要求往往也显得非常臃肿,重复高我们在正式介绍Refit这个项目之前,我们来看看我们在项目

  • 如何在ASP.NET Core中使用HttpClientFactory

    ASP.Net Core 是一个开源的,跨平台的,轻量级模块化框架,可用它来构建高性能的Web程序,这篇文章我们将会讨论如何在 ASP.Net Core 中使用 HttpClientFactory. 为什么要使用 HttpClientFactory 可以用 HttpClientFactory 来集中化管理 HttpClient,工厂提供了对 HttpClient 的创建,配置和调度,值得一提的是:HttpClient 一直都是 Http 请求业务方面的一等公民. HttpClient 虽好,但它

  • 如何在ASP.NET Core中使用ViewComponent

    前言 在之前的开发过程中,我们对于应用或者说使用一些小的组件,通常使用分布页(partial view),再往前在Web Form中我们会进行应用WEB Control,好吧提及一个关键性代码TagPrefix:TagName,通过这种的标记我们便可以在我们web form中进行引入我们的组件,当然自从.NET MVC之后呢,就已经没有了WebControl,而对于.NET Core后,又多了一个特性ViewComponent. 对于ViewComponent看起来它类似于小的控制器,而对于我们

随机推荐