.NET CORE中比較兩個(gè)文件內(nèi)容是否相同的最快方法
前言
最近項(xiàng)目有個(gè)需求,需要比較兩個(gè)任意大小文件的內(nèi)容是否相同,要求如下:
- 項(xiàng)目是.NET CORE,所以使用C#進(jìn)行編寫比較方法
- 文件大小任意,所以不能將文件內(nèi)容全部讀入到內(nèi)存中進(jìn)行比較(更專業(yè)點(diǎn)說(shuō),需要使用非緩存的比較方式)
- 不依賴第三方庫(kù)
- 越快越好
為了選出最優(yōu)的解決方案,我搭建了一個(gè)簡(jiǎn)單的命令行工程,準(zhǔn)備了兩個(gè)大小為912MB的文件,并且這兩個(gè)文件內(nèi)容完全相同.在本文的最后,你可以看到該工程的Main方法的代碼.
下面我們開(kāi)始嘗試各個(gè)比較方法,選出最優(yōu)的解決方案:
比較兩個(gè)文件是否完全相同,首先想到的是用哈希算法(如MD5,SHA)算出兩個(gè)文件的哈希值,然后進(jìn)行比較.
廢話少說(shuō),擼起袖子寫一個(gè)MD5比較方法:
/// <summary>
/// MD5
/// </summary>
/// <param name="file1"></param>
/// <param name="file2"></param>
/// <returns></returns>
private static bool CompareByMD5(string file1, string file2)
{
// 使用.NET內(nèi)置的MD5庫(kù)
using (var md5 = MD5.Create())
{
byte[] one, two;
using (var fs1 = File.Open(file1, FileMode.Open))
{
// 以FileStream讀取文件內(nèi)容,計(jì)算HASH值
one = md5.ComputeHash(fs1);
}
using (var fs2 = File.Open(file2, FileMode.Open))
{
// 以FileStream讀取文件內(nèi)容,計(jì)算HASH值
two = md5.ComputeHash(fs2);
}
// 將MD5結(jié)果(字節(jié)數(shù)組)轉(zhuǎn)換成字符串進(jìn)行比較
return BitConverter.ToString(one) == BitConverter.ToString(two);
}
}
比較結(jié)果:
Method: CompareByMD5, Identical: True. Elapsed: 00:00:05.7933178
耗時(shí)5.79秒,感覺(jué)還不錯(cuò).然而,這是最佳的解決方案嗎?
其實(shí)我們仔細(xì)想一下,答案應(yīng)該是否定的.
因?yàn)槿魏喂K惴ū举|(zhì)上都是對(duì)字節(jié)進(jìn)行一定的計(jì)算,而計(jì)算過(guò)程是要消耗時(shí)間的.
很多下載網(wǎng)站上提供了下載文件的哈希值,那是因?yàn)橄螺d的源文件本身不會(huì)改變,只需要計(jì)算一次源文件的哈希值,提供給用戶驗(yàn)證即可.
而我們的需求中,兩個(gè)文件都是不固定的,那么每次都要計(jì)算兩個(gè)文件的哈希值,就不太合適了.
所以,哈希比較這個(gè)方案被PASS.
這種求算法最優(yōu)解的問(wèn)題,我以往的經(jīng)驗(yàn)是: 去stackoverflow查找 :)
經(jīng)過(guò)我的艱苦努力,找到了一個(gè)非常切題的答案: How to compare 2 files fast using .NET?
得贊最多一個(gè)答案,將代碼改造了一下放入工程中:
/// <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個(gè)字節(jié)
int iterations = (int)Math.Ceiling((double)new FileInfo(file1).Length / BYTES_TO_READ); // 計(jì)算讀取次數(shù)
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++)
{
// 循環(huán)讀取到字節(jié)數(shù)組中
fs1.Read(one, 0, BYTES_TO_READ);
fs2.Read(two, 0, BYTES_TO_READ);
// 轉(zhuǎn)換為Int64進(jìn)行數(shù)值比較
if (BitConverter.ToInt64(one, 0) != BitConverter.ToInt64(two, 0))
return false;
}
}
return true;
}
該方法基本的原理是循環(huán)讀取兩個(gè)文件,每次讀取8個(gè)字節(jié),轉(zhuǎn)換為Int64,再進(jìn)行數(shù)值比較.那么效率如何呢?
Method: CompareByToInt64, Identical: True. Elapsed: 00:00:08.0918099
什么?8秒!竟然比MD5還慢?這不是SO得贊最多的答案嗎,怎么會(huì)這樣?
其實(shí)分析一下不難想到原因,因?yàn)槊看沃蛔x取8個(gè)字節(jié),程序頻繁的進(jìn)行IO操作,導(dǎo)致性能低下.看來(lái)SO上的答案也不能迷信啊!
那么優(yōu)化的方向就變?yōu)榱巳绾螠p少IO操作帶來(lái)的損耗.
既然每次8個(gè)字節(jié)太少了,我們定義一個(gè)大一些的字節(jié)數(shù)組,比如1024個(gè)字節(jié).每次讀取1024個(gè)字節(jié)到數(shù)組中,然后進(jìn)行字節(jié)數(shù)組的比較.
但是這樣又帶來(lái)一個(gè)新問(wèn)題,就是如何快速比較兩個(gè)字節(jié)數(shù)組是否相同?
我首先想到的是在MD5方法中用過(guò)的----將字節(jié)數(shù)組轉(zhuǎn)換成字符串進(jìn)行比較:
/// <summary>
/// 讀入到字節(jié)數(shù)組中比較(轉(zhuǎn)為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; // 兩個(gè)文件都讀取到了末尾,退出while循環(huán)
}
}
return true;
}
結(jié)果:
Method: CompareByString, Identical: True. Elapsed: 00:00:07.8088732
耗時(shí)也接近8秒,比上一個(gè)方法強(qiáng)不了多少.
分析一下原因,在每次循環(huán)中,字符串的轉(zhuǎn)換是一個(gè)非常耗時(shí)的操作.那么有沒(méi)有不進(jìn)行類型轉(zhuǎn)換的字節(jié)數(shù)組比較方法呢?
我想到了LINQ中有一個(gè)比較序列的方法SequenceEqual,我們嘗試使用該方法比較:
/// <summary>
/// 讀入到字節(jié)數(shù)組中比較(使用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; // 兩個(gè)文件都讀取到了末尾,退出while循環(huán)
}
}
return true;
}
結(jié)果:
Method: CompareBySequenceEqual, Identical: True. Elapsed: 00:00:08.2174360
竟然比前兩個(gè)都要慢(實(shí)際這也是所有方案中最慢的一個(gè)),LINQ的SequenceEqual看來(lái)不是為了效率而生.
那么我們不用那些花哨的功能,回歸質(zhì)樸,老實(shí)兒的使用while循環(huán)比較字節(jié)數(shù)組怎么樣呢?
/// <summary>
/// 讀入到字節(jié)數(shù)組中比較(while循環(huán)比較字節(jié)數(shù)組)
/// </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;
}
結(jié)果是....
Method: CompareByByteArry, Identical: True. Elapsed: 00:00:01.5356821
1.53秒!大突破!看來(lái)有時(shí)候看起來(lái)笨拙的方法反而效果更好!
試驗(yàn)到此,比較兩個(gè)900多MB的文件耗時(shí)1.5秒左右,讀者對(duì)于該方法是否滿意呢?
No!我不滿意!我相信通過(guò)努力,一定會(huì)找到更快的方法的!
同樣.NET CORE也在為了編寫高性能代碼而不斷的優(yōu)化中.
那么,我們?nèi)绾卫^續(xù)優(yōu)化我們的代碼呢?
我突然想到在C# 7.2中加入的一個(gè)新的值類型: Span<T>,它用來(lái)代表一段連續(xù)的內(nèi)存區(qū)域,并提供一系列可操作該區(qū)域的方法.
對(duì)于我們的需求,因?yàn)槲覀儾粫?huì)更改數(shù)組的值,所以可以使用另外一個(gè)只讀的類型ReadOnlySpan<T>追求更高的效率.
修改代碼,使用ReadOnlySpan<T>:
/// <summary>
/// 讀入到字節(jié)數(shù)組中比較(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);
// 字節(jié)數(shù)組可直接轉(zhuǎn)換為ReadOnlySpan
if (!((ReadOnlySpan<byte>)one).SequenceEqual((ReadOnlySpan<byte>)two)) return false;
if (len1 == 0 || len2 == 0) break; // 兩個(gè)文件都讀取到了末尾,退出while循環(huán)
}
}
return true;
}
核心是用來(lái)比較的SequenceEqual方法,該方法是ReadOnlySpan的一個(gè)擴(kuò)展方法,要注意它只是方法名與LINQ中一樣,實(shí)現(xiàn)完全不同.
那么該方法的表現(xiàn)如何呢?
Method: CompareByReadOnlySpan, Identical: True. Elapsed: 00:00:00.9287703
不 到 一 秒!
相對(duì)上一個(gè)已經(jīng)不錯(cuò)的結(jié)果,速度提高了差不多40%!
對(duì)此結(jié)果,我個(gè)人覺(jué)得已經(jīng)很滿意了,如果各位有更快的方法,請(qǐng)不吝賜教,我非常歡迎!
關(guān)于Span<T>結(jié)構(gòu)類型,各位讀者如有興趣,可瀏覽該文章,該文有非常詳細(xì)的介紹.
后記
文中的代碼只是出于實(shí)驗(yàn)性質(zhì),實(shí)際應(yīng)用中仍可以繼續(xù)細(xì)節(jié)上的優(yōu)化, 如:
- 如兩個(gè)文件大小不同,直接返回false
- 如果兩個(gè)文件路徑相同,直接返回true
- ...
試驗(yàn)工程的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);
}
}
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,謝謝大家對(duì)我們的支持。
上一篇:Asp.net core利用MediatR進(jìn)程內(nèi)發(fā)布/訂閱詳解
欄 目:ASP.NET
本文標(biāo)題:.NET CORE中比較兩個(gè)文件內(nèi)容是否相同的最快方法
本文地址:http://www.jygsgssxh.com/a1/ASP_NET/10916.html
您可能感興趣的文章
- 01-11如何給asp.net core寫個(gè)簡(jiǎn)單的健康檢查
- 01-11淺析.Net Core中Json配置的自動(dòng)更新
- 01-11.net core高吞吐遠(yuǎn)程方法如何調(diào)用組件XRPC詳解
- 01-11.NET Core 遷移躺坑記續(xù)集之Win下莫名其妙的超時(shí)
- 01-11.NET開(kāi)發(fā)人員關(guān)于ML.NET的入門學(xué)習(xí)
- 01-11docker部署Asp.net core應(yīng)用的完整步驟
- 01-11.net core webapi jwt 更為清爽的認(rèn)證詳解
- 01-11ASP.NET Core靜態(tài)文件的使用方法
- 01-11.NET Core 3.0之創(chuàng)建基于Consul的Configuration擴(kuò)展組件
- 01-11.net core EF Core調(diào)用存儲(chǔ)過(guò)程的方式


閱讀排行
- 1C語(yǔ)言 while語(yǔ)句的用法詳解
- 2java 實(shí)現(xiàn)簡(jiǎn)單圣誕樹(shù)的示例代碼(圣誕
- 3利用C語(yǔ)言實(shí)現(xiàn)“百馬百擔(dān)”問(wèn)題方法
- 4C語(yǔ)言中計(jì)算正弦的相關(guān)函數(shù)總結(jié)
- 5c語(yǔ)言計(jì)算三角形面積代碼
- 6什么是 WSH(腳本宿主)的詳細(xì)解釋
- 7C++ 中隨機(jī)函數(shù)random函數(shù)的使用方法
- 8正則表達(dá)式匹配各種特殊字符
- 9C語(yǔ)言十進(jìn)制轉(zhuǎn)二進(jìn)制代碼實(shí)例
- 10C語(yǔ)言查找數(shù)組里數(shù)字重復(fù)次數(shù)的方法
本欄相關(guān)
- 01-11vscode extension插件開(kāi)發(fā)詳解
- 01-11VsCode插件開(kāi)發(fā)之插件初步通信的方法
- 01-11如何給asp.net core寫個(gè)簡(jiǎn)單的健康檢查
- 01-11.net core高吞吐遠(yuǎn)程方法如何調(diào)用組件
- 01-11淺析.Net Core中Json配置的自動(dòng)更新
- 01-11.NET開(kāi)發(fā)人員關(guān)于ML.NET的入門學(xué)習(xí)
- 01-11.NET Core 遷移躺坑記續(xù)集之Win下莫名其
- 01-11.net core webapi jwt 更為清爽的認(rèn)證詳解
- 01-11docker部署Asp.net core應(yīng)用的完整步驟
- 01-11ASP.NET Core靜態(tài)文件的使用方法
隨機(jī)閱讀
- 01-11ajax實(shí)現(xiàn)頁(yè)面的局部加載
- 08-05織夢(mèng)dedecms什么時(shí)候用欄目交叉功能?
- 01-10使用C語(yǔ)言求解撲克牌的順子及n個(gè)骰子
- 08-05dedecms(織夢(mèng))副欄目數(shù)量限制代碼修改
- 01-11Mac OSX 打開(kāi)原生自帶讀寫NTFS功能(圖文
- 04-02jquery與jsp,用jquery
- 01-10delphi制作wav文件的方法
- 01-10C#中split用法實(shí)例總結(jié)
- 01-10SublimeText編譯C開(kāi)發(fā)環(huán)境設(shè)置
- 08-05DEDE織夢(mèng)data目錄下的sessions文件夾有什


