前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >.NET 下最快比较两个文件内容是否相同

.NET 下最快比较两个文件内容是否相同

作者头像
郑子铭
发布于 2023-08-30 02:37:34
发布于 2023-08-30 02:37:34
39300
代码可运行
举报
运行总次数:0
代码可运行

最近项目有个需求,需要比较两个任意大小文件的内容是否相同,要求如下:

  1. 项目是.NET Core,所以使用C#进行编写比较方法
  2. 文件大小任意,所以不能将文件内容全部读入到内存中进行比较(更专业点说,需要使用非缓存的比较方式)
  3. 不依赖第三方库
  4. 越快越好

为了选出最优的解决方案,我搭建了一个简单的命令行工程,准备了两个大小为912MB的文件,并且这两个文件内容完全相同.在本文的最后,你可以看到该工程的Main方法的代码.

下面我们开始尝试各个比较方法,选出最优的解决方案:

比较两个文件是否完全相同,首先想到的是用哈希算法(如MD5,SHA)算出两个文件的哈希值,然后进行比较.

写一个MD5比较方法:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/// <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);
    }
}

比较结果:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Method: CompareByMD5, Identical: True. Elapsed: 00:00:05.7933178

耗时5.79秒,感觉还不错.然而,这是最佳的解决方案吗?

其实我们仔细想一下,答案应该是否定的.

因为任何哈希算法本质上都是对字节进行一定的计算,而计算过程是要消耗时间的.

很多下载网站上提供了下载文件的哈希值,那是因为下载的源文件本身不会改变,只需要计算一次源文件的哈希值,提供给用户验证即可.

而我们的需求中,两个文件都是不固定的,那么每次都要计算两个文件的哈希值,就不太合适了.

所以,哈希比较这个方案被PASS.

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

经过我的艰苦努力,找到了一个非常切题的答案: How to compare 2 files fast using .NET? https://stackoverflow.com/questions/1358510/how-to-compare-2-files-fast-using-net/1359947#1359947

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

该方法基本的原理是循环读取两个文件,每次读取8个字节,转换为Int64,再进行数值比较.那么效率如何呢?

Method: CompareByToInt64, Identical: True. Elapsed: 00:00:08.0918099

什么?8秒!竟然比MD5还慢?这不是SO得赞最多的答案吗,怎么会这样?

其实分析一下不难想到原因,因为每次只读取8个字节,程序频繁的进行IO操作,导致性能低下.看来SO上的答案也不能迷信啊!

那么优化的方向就变为了如何减少IO操作带来的损耗.

既然每次8个字节太少了,我们定义一个大一些的字节数组,比如1024个字节.每次读取1024个字节到数组中,然后进行字节数组的比较.

但是这样又带来一个新问题,就是如何快速比较两个字节数组是否相同?

我首先想到的是在MD5方法中用过的----将字节数组转换成字符串进行比较:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/// <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;
}

结果:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Method: CompareByString, Identical: True. Elapsed: 00:00:07.8088732

耗时也接近8秒,比上一个方法强不了多少.

分析一下原因,在每次循环中,字符串的转换是一个非常耗时的操作.那么有没有不进行类型转换的字节数组比较方法呢?

我想到了LINQ中有一个比较序列的方法SequenceEqual,我们尝试使用该方法比较:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/// <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;
}

结果:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Method: CompareBySequenceEqual, Identical: True. Elapsed: 00:00:08.2174360

竟然比前两个都要慢(实际这也是所有方案中最慢的一个),LINQ的SequenceEqual看来不是为了效率而生.

那么我们不用那些花哨的功能,回归质朴,老实儿的使用while循环比较字节数组怎么样呢?

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/// <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;
}

结果是....

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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>:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/// <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中一样,实现完全不同.

那么该方法的表现如何呢?

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Method: CompareByReadOnlySpan, Identical: True. Elapsed: 00:00:00.9287703

不 到 一 秒!

相对上一个已经不错的结果,速度提高了差不多40%!

对此结果,我个人觉得已经很满意了,如果各位有更快的方法,请不吝赐教,我非常欢迎!

关于Span<T>结构类型,各位读者如有兴趣,可浏览该文章,该文有非常详细的介绍.

后记

文中的代码只是出于实验性质,实际应用中仍可以继续细节上的优化, 如:

  • 如两个文件大小不同,直接返回false
  • 如果两个文件路径相同,直接返回true
  • ...

试验工程的Main方法源码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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);
    }
}
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-08-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 DotNet NB 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
张益唐吟了两句诗,2万人点赞
这不是玩梗!最近震动数学界并出圈的数学家张益唐,刚刚在知乎朗道-西格尔零点问题的相关提问下,亲笔写下了回答:
量子位
2022/12/08
3380
张益唐吟了两句诗,2万人点赞
见证历史!数学家张益唐北大讲座:本质上已证明“零点猜想”,111 页论文已公开
有数论学者表示,张益唐有关朗道 - 西格尔零点猜想的论文结果意义重大,使得以前的很多结果从假设性结果变成了确定性结果。
深度学习与Python
2022/11/28
4670
见证历史!数学家张益唐北大讲座:本质上已证明“零点猜想”,111 页论文已公开
15年磨一剑!张益唐111页「零点猜想」论文终于来了
昨天,著名华裔数学家张益唐教授在攻克数学界著名难题之一——郎道-西格尔零点猜想问题的道路上再进一步!
新智元
2023/01/07
6120
15年磨一剑!张益唐111页「零点猜想」论文终于来了
万字一手实测Prover-V2-671B数学证明模型
上一篇比较简单的文章,其实已经介绍了昨天DeepSeek-Prover-V2-671B的相关介绍,本来已经是节假日了,想着不写了。
AIGC新知
2025/05/01
1390
万字一手实测Prover-V2-671B数学证明模型
张益唐111页零点猜想论文出炉!自称比孪生素数猜想意义更大,每天思考12小时被太太骂
根据知乎博主“TravorLZH”的介绍,十九世纪的数学家为了研究素数分布引入了黎曼猜想。
量子位
2022/12/08
5320
张益唐111页零点猜想论文出炉!自称比孪生素数猜想意义更大,每天思考12小时被太太骂
【机器学习】机器学习引领数学难题攻克:迈向未知数学领域的新突破
机器学习算法背后离不开坚实的数学理论支撑。例如,线性代数在数据表示和矩阵运算中起着关键作用。在处理高维数据时,我们常常将数据表示为矩阵形式,通过矩阵的乘法、转置等运算进行数据变换和特征提取。
学无止尽5
2025/01/20
1590
张益唐被曝已证明黎曼猜想相关问题,震动数学界
网传数学家张益唐,已经攻克了朗道-西格尔零点猜想(Landau-Siegel Zeros Conjecture)。
量子位
2022/12/08
3110
张益唐被曝已证明黎曼猜想相关问题,震动数学界
15年磨一剑:张益唐证明黎曼猜想相关问题?11月论文见
这两天,张益唐「攻克」朗道-西格尔零点猜想(Landau-Siegel Zeros Conjecture)的传闻铺天盖地。
新智元
2023/01/06
8590
15年磨一剑:张益唐证明黎曼猜想相关问题?11月论文见
刚刚,华人数学家张益唐宣称攻克Landau-Siegel零点猜想
据乐生活与爱IT Plus报道,10月15日,张益唐老师在北大校友会组织的沙龙中提到,自己做完了Landau-Siegel猜想。
新智元
2023/01/06
4710
刚刚,华人数学家张益唐宣称攻克Landau-Siegel零点猜想
111页,张益唐攻克朗道-西格尔零点猜想论文公布,8号B站直播开讲
终于!数学家张益唐攻克朗道 - 西格尔(Landau-Siegel)零点猜想的预览版论文放出了。
机器之心
2022/12/15
4230
111页,张益唐攻克朗道-西格尔零点猜想论文公布,8号B站直播开讲
陶哲轩力推36岁菲尔兹奖得主新论文,指向黎曼猜想重大突破!
「千禧年七大数学难题」之一——黎曼猜想(Riemann hypothesis,RH),刚刚取得显著突破,数学家们距离摘取「猜想界的皇冠」又近了一步!
新智元
2024/06/17
1580
陶哲轩力推36岁菲尔兹奖得主新论文,指向黎曼猜想重大突破!
陶哲轩:张益唐新论文存在一些技术问题,我已请他澄清
具体来说,陶哲轩列出了论文中一些方程引用缺失,集中在63-67页、70页、98-99页,以及结尾的109页。
量子位
2022/12/09
9090
陶哲轩:张益唐新论文存在一些技术问题,我已请他澄清
黎曼猜想显著突破!陶哲轩强推MIT、牛津新论文,37岁菲尔兹奖得主参与
黎曼猜想是数学中一个非常重要的未解决问题,与素数分布的精确性质有关(素数是那些只能被 1 和自身整除的数字,它们在数论中扮演着基础性的角色)。
机器之心
2024/06/17
2130
黎曼猜想显著突破!陶哲轩强推MIT、牛津新论文,37岁菲尔兹奖得主参与
黎曼猜想突破作者首次公开讲解,陶哲轩送上总结
MIT 数学教授 Larry Guth 和牛津大学数学研究所教授、2022 菲尔兹奖得主 James Maynard 撰写论文《New large value estimates for Dirichlet polynomials》,首次对数学家 Albert Ingham 在 1940 年左右关于黎曼 ζ 函数零点(以及更广泛地控制各种 Dirichlet 级数的大值)的经典界限做出了实质性改进。
机器之心
2024/06/17
1980
黎曼猜想突破作者首次公开讲解,陶哲轩送上总结
17岁高中生证明数学界存在27年难题,「他的论文值得任何数学家为之自豪」
回想一下,你的高中在干什么,有没有值得骄傲的一件事。本文我们将要介绍的这位学生,名叫 Daniel Larsen,在高中的最后一年里,他证明了卡迈克尔数(Carmichael numbers)的关键定理。在他发表了自己的证明后,Larsen 被 MIT 录取,主修数学。
机器之心
2022/12/15
4340
17岁高中生证明数学界存在27年难题,「他的论文值得任何数学家为之自豪」
素数那些事
在我们刚开始编写程序的时候,往往会要求写一个输出n以内(n大于等于2)的所有素数。首先来介绍一下什么是素数。有些数具有特殊的属性,它们不能被表示为两个较小的数字的乘积,如2,3,5,7,等等。这样的数称为素数(或质数),在纯数学和应用数学领域,它们发挥了重要的作用。所有的自然数中的素数的分布并不遵循任何规律。然而还是有人提出了素数分布的规律,比如数学家波恩哈德·黎曼于1859年提出了黎曼猜想。今天我们就来谈谈黎曼猜想。
不可言诉的深渊
2019/07/26
8360
素数之魂——黎曼和他的伟大猜想
作者:卢昌海 博客:http://www.changhai.org 摘自:南方周末 导读 与费尔马猜想时隔三个半世纪以上才被解决,哥德巴赫猜想历经两个半世纪以上屹立不倒相比,黎曼猜想只有一个半世纪的纪录还差得很远,但它在数学上的重要性要远远超过这两个大众知名度更高的猜想。黎曼猜想是当今数学界最重要、最期待解决的数学难题。 黎曼(1826-1866)是历史上最具想象力的数学家之一 1 2000年5月24日,美国克雷数学研究所在法国巴黎召开了一次数学会议。在会议上,与会者们列出了七个数学难题,并作出了一个颇具轰
大数据文摘
2018/05/22
1K0
Grok 3证明黎曼猜想,训练遭灾难性事件?数学家称不夸张,两年内AI将解出千禧年难题
为此,xAI暂停了Grok 3的训练来验证它的证明,如果结果是正确的,将会完全终止模型的训练。
新智元
2025/02/14
720
Grok 3证明黎曼猜想,训练遭灾难性事件?数学家称不夸张,两年内AI将解出千禧年难题
只用300小时,17岁高中生解开困扰数学家27年难题,因张益唐「入坑」数论
只因在电视上多看了一眼数学家张益唐的纪录片,中学生开始沉迷数论,还独立发表了一篇“博士级别”数学论文。
量子位
2022/12/08
3770
只用300小时,17岁高中生解开困扰数学家27年难题,因张益唐「入坑」数论
天才数学家连续拿下菲尔兹奖、新视野奖,专攻“最难的简单问题”,生活中还是个社牛
羿阁 丰色 发自 凹非寺 量子位 | 公众号 QbitAI 天天一身白衬衣+牛仔裤、为了专心想题走路不戴眼镜…… 但又非常擅长社交、3岁就会“整蛊”大人、不止研究搞得好还特别会摄影。 ——这样的数学家你见过吗? 他就是今年七月刚刚摘下菲尔兹奖的牛津大学教授詹姆斯·梅纳德 (James Maynard)。 此前因为优化张益唐的“孪生素数猜想”结果一战成名,连华裔数学天才陶哲轩都对初出茅庐的他赞不绝口。 现在,他又拿下了2023科学突破奖下的数学新视野奖,将10万美元奖金收入囊中。 年仅35岁的他,因为在数论
量子位
2022/10/08
6460
天才数学家连续拿下菲尔兹奖、新视野奖,专攻“最难的简单问题”,生活中还是个社牛
推荐阅读
张益唐吟了两句诗,2万人点赞
3380
见证历史!数学家张益唐北大讲座:本质上已证明“零点猜想”,111 页论文已公开
4670
15年磨一剑!张益唐111页「零点猜想」论文终于来了
6120
万字一手实测Prover-V2-671B数学证明模型
1390
张益唐111页零点猜想论文出炉!自称比孪生素数猜想意义更大,每天思考12小时被太太骂
5320
【机器学习】机器学习引领数学难题攻克:迈向未知数学领域的新突破
1590
张益唐被曝已证明黎曼猜想相关问题,震动数学界
3110
15年磨一剑:张益唐证明黎曼猜想相关问题?11月论文见
8590
刚刚,华人数学家张益唐宣称攻克Landau-Siegel零点猜想
4710
111页,张益唐攻克朗道-西格尔零点猜想论文公布,8号B站直播开讲
4230
陶哲轩力推36岁菲尔兹奖得主新论文,指向黎曼猜想重大突破!
1580
陶哲轩:张益唐新论文存在一些技术问题,我已请他澄清
9090
黎曼猜想显著突破!陶哲轩强推MIT、牛津新论文,37岁菲尔兹奖得主参与
2130
黎曼猜想突破作者首次公开讲解,陶哲轩送上总结
1980
17岁高中生证明数学界存在27年难题,「他的论文值得任何数学家为之自豪」
4340
素数那些事
8360
素数之魂——黎曼和他的伟大猜想
1K0
Grok 3证明黎曼猜想,训练遭灾难性事件?数学家称不夸张,两年内AI将解出千禧年难题
720
只用300小时,17岁高中生解开困扰数学家27年难题,因张益唐「入坑」数论
3770
天才数学家连续拿下菲尔兹奖、新视野奖,专攻“最难的简单问题”,生活中还是个社牛
6460
相关推荐
张益唐吟了两句诗,2万人点赞
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档