【问题标题】:Fast checksum hashing?快速校验和散列?
【发布时间】:2011-10-29 02:26:21
【问题描述】:

编写一个简单的程序,可以在我的计算机上找到完全重复的文件,但它有点慢。有什么办法可以加快速度吗?

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Security.Cryptography;

namespace DupeFinder
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Getting files...");
            var files = Directory.GetFiles(@"D:\Photos", "*", SearchOption.AllDirectories);
            var alg = new HMACMD5();
            var dict = new Dictionary<string,List<string>>();
            Console.WriteLine("Computing hashes...");
            int i=0;
            int cursorLeft = Console.CursorLeft;
            int cursorTop = Console.CursorTop;
            foreach(var fileName in files)
            {
                Console.SetCursorPosition(cursorLeft,cursorTop);
                Console.Write("Hashing file {0}/{1}", ++i, files.Length);
                using(var stream = new BufferedStream(File.OpenRead(fileName),1024*1024*5))
                {
                    var hash = alg.ComputeHash(stream);
                    var str = BitConverter.ToString(hash);
                    if (!dict.ContainsKey(str)) dict[str] = new List<string>();
                    dict[str].Add(fileName);
                }
            }
            Console.WriteLine();

            foreach(var dupe in dict.Where(p => p.Value.Count >= 2))
            {
                Console.WriteLine(string.Join(", ", dupe.Value));
            }

            Console.WriteLine("Done!");
            Console.ReadLine();
        }
    }
}

可能的优化:

  1. 避免先将字节数组转换为字符串。我试过了,但它不起作用,我猜是因为它使用引用等于而不是比较字节。
  2. 更快的哈希算法?
  3. 不同的流或缓冲区大小?我试着做 1024^3 应该是一个兆字节,但这似乎会减慢它的速度。

或者这只是一件天生缓慢的事情?


我记得字典可以接受IEqualityComparer,所以我可以编写自己的byte[] 比较器。

我在互联网上找到的很多算法都倾向于先比较字节长度,我不需要这样做,因为我知道它总是 16 字节。他们也倾向于一次比较 1 个字节......但我在 64 位机器上,那为什么不做 8 个呢?

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Security.Cryptography;

namespace DupeFinder
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Getting files...");
            string dir = @"D:\Photos";
            var files = Directory.GetFiles(dir, "*", SearchOption.AllDirectories);
            var alg = new HMACMD5();
            var dict = new Dictionary<byte[], List<string>>(new Md5Comparer());
            Console.WriteLine("Computing hashes...");
            int i = 0;
            int cursorLeft = Console.CursorLeft;
            int cursorTop = Console.CursorTop;
            foreach (var fileName in files)
            {
                Console.SetCursorPosition(cursorLeft, cursorTop);
                Console.Write("Hashing file {0}/{1}", ++i, files.Length);
                using (var stream = new BufferedStream(File.OpenRead(fileName), 1024 * 1024 * 5))
                {
                    var hash = alg.ComputeHash(stream);
                    if (!dict.ContainsKey(hash)) dict[hash] = new List<string>();
                    dict[hash].Add(fileName);
                }
            }
            Console.WriteLine();

            using (var sw = new StreamWriter(Path.Combine(dir, "duplicates.txt")))
            {
                i = 0;
                foreach (var dupe in dict.Where(p => p.Value.Count >= 2))
                {
                    sw.WriteLine("Duplicate {0}", ++i);
                    foreach(var fn in dupe.Value)
                    {
                        sw.WriteLine("- {0}", fn);
                    }
                }
            }

            Console.WriteLine("Done!");
            //Console.ReadLine();
        }
    }

    class Md5Comparer : IEqualityComparer<byte[]>
    {
        public bool Equals(byte[] x, byte[] y)
        {
            var xi = BitConverter.ToInt64(x, 0);
            var yi = BitConverter.ToInt64(y, 0);
            if (xi != yi) return false;
            xi = BitConverter.ToInt64(x, 8);
            yi = BitConverter.ToInt64(y, 8);
            return xi == yi;
        }

        public int GetHashCode(byte[] obj)
        {
            return obj[0];
        }
    }
}

不知道这有多快......我没有做过任何基准测试,但它肯定看起来并没有变慢。


新代码,感谢@spender:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Security.Cryptography;

namespace DupeFinder
{
    class Program
    {
        static void Main(string[] args)
        {
            var watch = Stopwatch.StartNew();
            const string dir = @"D:\Photos";
            var md5Comparer = new Md5Comparer();

            var dupeGroups = Directory.EnumerateFiles(dir, "*", SearchOption.AllDirectories)
                .Select(fn => new FileInfo(fn))
                .GroupBy(fi => fi.Length)
                .Where(g => g.Count() > 1)
                .SelectMany(g => g
                   .GroupBy(fi => GetHash(fi.FullName), md5Comparer)
                   .Where(g2 => g2.Count() > 1));

            using (var sw = new StreamWriter(Path.Combine(dir, "duplicates.txt")))
            {
                int i = 0;
                foreach (var dupeGroup in dupeGroups)
                {
                    sw.WriteLine("Duplicate {0}", ++i);
                    foreach(FileInfo fi in dupeGroup)
                    {
                        sw.WriteLine("- {0}", fi.FullName);
                    }
                }
            }

            Console.WriteLine("{0:0.000} seconds", watch.ElapsedMilliseconds / 1000d); // 22.068 seconds to process 10K files, 37 GB, 463 dupes
            Console.ReadLine();
        }

        static readonly HMACMD5 md5Hasher = new HMACMD5();
        public static byte[] GetHash(string fileName)
        {
            using(var stream = File.OpenRead(fileName))
                return md5Hasher.ComputeHash(stream);
        }
    }

    class Md5Comparer : IEqualityComparer<byte[]>
    {
        public bool Equals(byte[] x, byte[] y)
        {
            var xi = BitConverter.ToInt64(x, 0);
            var yi = BitConverter.ToInt64(y, 0);
            if (xi != yi) return false;
            xi = BitConverter.ToInt64(x, 8);
            yi = BitConverter.ToInt64(y, 8);
            return xi == yi;
        }

        public int GetHashCode(byte[] obj)
        {
            return obj[0];
        }
    }
}

从 360+ 秒减少到 22-70 秒。进步很大!

【问题讨论】:

  • 您是否在比较大量不断重新缓冲的大文件,使用 (var stream = new BufferedStream(File.OpenRead(fileName), 1024 * 1024 * 5)) 更改为 (var stream = File .OpenRead(fileName)) 在我的系统上执行得更快。
  • @Gary.S:是的...我只是尝试过。好像快了一点。我主要是散列照片。平均文件大小显然是 280 KiB。不用等……那是糟糕的数学。 3.64 MB。
  • 您正在测试多少个文件以及需要多长时间。我让它在大约 4 秒内运行 9600 个文件。总大小约为 760 MB。
  • @Gary.S:10K 文件,37 GB。需要几分钟。
  • 1024^3(如果意思是1024*1024*1024)是1Gb,不是1Mb

标签: c# optimization


【解决方案1】:

你错过了一个巨大的选择:如果尺寸不匹配......

此外,匹配的哈希值并不能保证匹配的内容。

编辑

好的。找到基于大小的骗子并不难,然后您可以检查它们是否真的是骗子:

例如:

var files = 
    Directory
        .GetFiles(
            @"C:\Users\Administrator\Downloads",
            "*", 
            SearchOption.AllDirectories);
var fileGroups=
    files
        .Select(fn => new FileInfo(fn))
        .GroupBy(fi => fi.Length)
        .Where(g => g.Count()>1);

您可以通过为给定文件名创建散列函数来更进一步

int GetHash(fileName)  //fill in your definition

那么……

var fg2 =
        fileGroups
            .SelectMany(
                g => g
                    .GroupBy(fi => GetHash(fi.FullName))
                    .Where(gg=>gg.Count()>1));

foreach(var dupeGroup in fg2)
{
    //everything in this group is probably duplicate
    //although you'd need to do byte by byte to be sure
    foreach(FileInfo dupe in dupeGroup)
    {

    }
}

通过这样做,您将大大减少所需的散列量,因为您已按大小预先过滤了候选对象。

【讨论】:

  • 我将如何比较文件大小?当您有 2 个文件并且想要将一个文件与另一个文件进行比较时,这有效,但当您想将一个文件与其他每个文件进行比较时则不行......除非我构建了一个 2 级字典,第一个是文件 -大小,第二个是哈希。关于你的第二点......这两个文件具有相同哈希但不完全相同的概率非常低。这就是哈希的全部目的。
  • 其次,即使我确实构建了一个 2 级字典,它也会立即告诉我是否没有完全匹配,而无需在许多情况下进入 2 级,但我仍然有计算哈希以供将来比较。它只会让我不必在许多情况下比较 2 个哈希值……但是由于增加了复杂性,我不肯定比较 int+string 比比较 2 个字符串便宜。如果我错了,请告诉我......我只是想按照你的逻辑。
  • 我不需要先将字节数组转换为字符串,现在我只是比较两对整数,这可能比比较文件大小便宜(我不必查询文件系统)。如果您仍然认为它是一个巨大的光电器件,请告诉我我在这里忽略了什么。我真的很好奇。
  • 基本上,您想查看所有文件并只考虑大小相同的文件。所以,用这个属性生成一个字典,然后只散列相同大小的字典。这将给予消费者正在评论的戏剧性加速。您还可以在实际进行哈希之前检查一些其他琐碎的特定于文件的内容(标题等)。然后,如果哈希检查相同,则对该文件逐字节执行。就是这个想法。
  • @NoonSilk 和 Spender:哦....当您首先按文件大小对它们进行分组时,这更有意义。我想我不必对组大小为 1 的文件进行哈希处理。我稍后再试一下……现在听起来确实快了很多:D 谢谢大家。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2010-09-12
  • 2017-08-13
  • 1970-01-01
  • 2011-12-10
  • 1970-01-01
  • 2015-03-12
相关资源
最近更新 更多