【问题标题】:How do I generate a hashcode from a byte array in C#?如何从 C# 中的字节数组生成哈希码?
【发布时间】:2010-09-06 04:35:42
【问题描述】:

假设我有一个存储字节数组的对象,我希望能够有效地为其生成哈希码。我过去为此使用过加密哈希函数,因为它们很容易实现,但是它们做的工作比加密单向做的要多得多,而且我不在乎(我只是在使用哈希码作为哈希表的键)。

这是我今天拥有的:

struct SomeData : IEquatable<SomeData>
{
    private readonly byte[] data;
    public SomeData(byte[] data)
    {
        if (null == data || data.Length <= 0)
        {
            throw new ArgumentException("data");
        }
        this.data = new byte[data.Length];
        Array.Copy(data, this.data, data.Length);
    }

    public override bool Equals(object obj)
    {
        return obj is SomeData && Equals((SomeData)obj);
    }

    public bool Equals(SomeData other)
    {
        if (other.data.Length != data.Length)
        {
            return false;
        }
        for (int i = 0; i < data.Length; ++i)
        {
            if (data[i] != other.data[i])
            {
                return false;
            }
        }
        return true;
    }
    public override int GetHashCode()
    {
        return BitConverter.ToInt32(new MD5CryptoServiceProvider().ComputeHash(data), 0);
    }
}

有什么想法吗?


dp:你说得对,我错过了 Equals 的检查,我已经更新了它。使用字节数组中的现有哈希码将导致引用相等(或至少将相同的概念转换为哈希码)。 例如:

byte[] b1 = new byte[] { 1 };
byte[] b2 = new byte[] { 1 };
int h1 = b1.GetHashCode();
int h2 = b2.GetHashCode();

使用该代码,尽管两个字节数组在其中具有相同的值,但它们指的是内存的不同部分,并且会导致(可能)不同的哈希码。我需要内容相同的两个字节数组的哈希码相等。

【问题讨论】:

    标签: c# hash


    【解决方案1】:

    对象的哈希码不需要是唯一的。

    检查规则是:

    • 哈希码是否相等?然后调用完整(慢)Equals 方法。
    • 哈希码不相等吗?那么这两项肯定不相等。

    您想要的只是一个GetHashCode 算法,它将您的集合分成大致均匀的组 - 它不应该形成键,因为HashTableDictionary&lt;&gt; 需要使用哈希来优化检索。

    您预计数据会保留多长时间?随机性如何?如果长度变化很大(比如文件),那么只返回长度。如果长度可能相似,请查看变化的字节子集。

    GetHashCode 应该比Equals 快很多,但不需要是唯一的。

    两个相同的东西绝不能有不同的哈希码。两个不同的对象不应该具有相同的哈希码,但是会发生一些冲突(毕竟,排列比可能的 32 位整数要多)。

    【讨论】:

    • +1 这是我听过的最清楚的解释之一,为什么重写 Equals GetHashcode 是有益的。
    【解决方案2】:

    不要对哈希表使用加密哈希,这很荒谬/矫枉过正。

    来吧...在 C# 中修改 FNV 哈希

    http://bretm.home.comcast.net/hash/6.html

        public static int ComputeHash(params byte[] data)
        {
            unchecked
            {
                const int p = 16777619;
                int hash = (int)2166136261;
    
                for (int i = 0; i < data.Length; i++)
                    hash = (hash ^ data[i]) * p;
    
                hash += hash << 13;
                hash ^= hash >> 7;
                hash += hash << 3;
                hash ^= hash >> 17;
                hash += hash << 5;
                return hash;
            }
        }
    

    【讨论】:

    • 这将产生非常独特的哈希值,但对于GetHashCode 确实不起作用。这个想法是哈希允许集合在使用较慢的Equals 之前检查两个byte[] 是否匹配的快速方法。在这个实现中,您正在循环整个数组,因此对于非常大的数组,相等性检查可能会快很多。这是计算通用哈希的好方法,但对于 .Net 实际使用 GetHashCode 的方式,这实际上可能会减慢收集速度。
    • @tigrou - 我并不是说这不是一个有用的散列机制,但你不应该将它用于GetHashCode 实现,因为.Net 散列集合都假定GetHashCode将比Equals 快几个数量级。事实上,如果GetHashCode 检查通过,他们将继续调用Equals,因为预计会有一定数量的冲突。如果这两种方法都循环整个集合,你会得到一个非常慢的 HashTableDictionary
    • @Keith - 你错了。关键是 GetHashCode() 必须只调用一次,而 Equals() 必须在每次比较时调用。因此,哈希计算的运行时间比 equals 更长是完全可以的。事实上,内置的 .NET 字符串散列就是这样做的。
    • @Keith:kaalus 是正确的。一个好的散列码必须包含来自要散列的整个对象的信息,包括所有属性和字段值。没有办法避免每次调用都扫描此信息,除非相关对象是不可变的并且在创建时缓存哈希码。
    • 值得注意的是,链接页面(这里的缓存版本-archive.is/MnmRY)实际上使用了uint,因此会产生不同的哈希值。
    【解决方案3】:

    借用JetBrains软件生成的代码,我确定了这个函数:

        public override int GetHashCode()
        {
            unchecked
            {
                var result = 0;
                foreach (byte b in _key)
                    result = (result*31) ^ b;
                return result;
            }
        }
    

    仅对字节进行异或运算的问题在于,返回值的 3/4(3 个字节)只有 2 个可能的值(全部打开或全部关闭)。这会分散更多信息。

    在 Equals 中设置断点是一个不错的建议。将我的数据的大约 200,000 个条目添加到字典中,会看到大约 10 个 Equals 调用(或 1/20,000)。

    【讨论】:

    • for IList&lt;byte&gt; 绝对使用基于索引的 for 循环而不是 foreach。可能与byte[] 没有太大区别,因为foreach 将在内部转换为for
    • foreach 循环有时会在 List 上循环时被编译成 for 循环,不确定在 IList 上循环时是否也会发生这种情况(它总是应该慢一点,对于 big数组,但对于小数组 => foreach 比 for 有更多的初始化。
    【解决方案4】:

    你和SHA1CryptoServiceProvider.ComputeHash方法比较过吗?它需要一个字节数组并返回一个 SHA1 哈希,我相信它已经得到了很好的优化。我在Identicon Handler 中使用了它,它在负载下表现得非常好。

    【讨论】:

    • SHA1 比 MD5 慢。如果您不担心安全性,请使用 MD5。
    • 感谢 Jon .. SHA1CryptoServiceProvider.ComputeHash 方法对我有用..!!
    【解决方案5】:

    我发现了有趣的结果:

    我有课:

    public class MyHash : IEquatable<MyHash>
    {        
        public byte[] Val { get; private set; }
    
        public MyHash(byte[] val)
        {
            Val = val;
        }
    
        /// <summary>
        /// Test if this Class is equal to another class
        /// </summary>
        /// <param name="other"></param>
        /// <returns></returns>
        public bool Equals(MyHash other)
        {
            if (other.Val.Length == this.Val.Length)
            {
                for (var i = 0; i < this.Val.Length; i++)
                {
                    if (other.Val[i] != this.Val[i])
                    {
                        return false;
                    }
                }
    
                return true;
            }
            else
            {
                return false;
            }            
        }
    
        public override int GetHashCode()
        {            
            var str = Convert.ToBase64String(Val);
            return str.GetHashCode();          
        }
    }
    

    然后我创建了一个带有 MyHash 类型键的字典,以测试我可以插入多快,并且我还可以知道有多少冲突。我做了以下

            // dictionary we use to check for collisions
            Dictionary<MyHash, bool> checkForDuplicatesDic = new Dictionary<MyHash, bool>();
    
            // used to generate random arrays
            Random rand = new Random();
    
    
    
            var now = DateTime.Now;
    
            for (var j = 0; j < 100; j++)
            {
                for (var i = 0; i < 5000; i++)
                {
                    // create new array and populate it with random bytes
                    byte[] randBytes = new byte[byte.MaxValue];
                    rand.NextBytes(randBytes);
    
                    MyHash h = new MyHash(randBytes);
    
                    if (checkForDuplicatesDic.ContainsKey(h))
                    {
                        Console.WriteLine("Duplicate");
                    }
                    else
                    {
                        checkForDuplicatesDic[h] = true;
                    }
                }
                Console.WriteLine(j);
                checkForDuplicatesDic.Clear(); // clear dictionary every 5000 iterations
            }
    
            var elapsed = DateTime.Now - now;
    
            Console.Read();
    

    每次我向字典中插入一个新项目时,字典都会计算该对象的哈希值。因此,您可以通过将此处找到的几个答案放在方法public override int GetHashCode() 中来判断哪种方法最有效。迄今为止最快且冲突次数最少的方法是:

        public override int GetHashCode()
        {            
            var str = Convert.ToBase64String(Val);
            return str.GetHashCode();          
        }
    

    执行需要 2 秒。方法

        public override int GetHashCode()
        {
            // 7.1 seconds
            unchecked
            {
                const int p = 16777619;
                int hash = (int)2166136261;
    
                for (int i = 0; i < Val.Length; i++)
                    hash = (hash ^ Val[i]) * p;
    
                hash += hash << 13;
                hash ^= hash >> 7;
                hash += hash << 3;
                hash ^= hash >> 17;
                hash += hash << 5;
                return hash;
            }
        }
    

    也没有碰撞,但执行需要 7 秒!

    【讨论】:

    • 你能解释一下你的哈希算法吗
    【解决方案6】:

    如果您正在寻找性能,我测试了一些哈希键,并且 我推荐Bob Jenkin's hash function。都快疯了 计算并且将给出与密码一样少的冲突 您到现在为止使用的哈希值。

    我完全不懂C#,也不知道能不能和C链接,但是 这里是its implementation in C

    【讨论】:

    • 您可以通过 pinvoke 从 c# 调用 c 函数。它有一些性能影响(例如传递参数的固定和编组 - 如何取决于实际使用的类型),但在不频繁调用它们时可以忽略不计(这意味着在循环中 > 数千次)。甚至一些图形渲染框架(即 OpenTK、SkiaSharp)使用了大量的 pinvoke 调用,性能仍然不错。
    【解决方案7】:

    使用字节数组字段中的现有哈希码是否不够好?另请注意,在 Equals 方法中,您应该在进行比较之前检查数组的大小是否相同。

    【讨论】:

      【解决方案8】:

      生成一个好的哈希说起来容易做起来难。请记住,您基本上是用 m 位信息表示 n 字节数据。您的数据集越大,m 越小,发生冲突的可能性就越大……两条数据解析为相同的哈希值。

      我学过的最简单的散列就是简单地将所有字节异或在一起。它比大多数复杂的散列算法和用于小型数据集的通用散列算法更容易、更快。真的是散列算法的冒泡排序。由于简单的实现会给你留下 8 位,那只有 256 个哈希......不是那么热。你可以异或块而不是单个字节,但是算法变得更加复杂。

      当然,加密算法可能正在做一些你不需要的事情......但它们也是通用哈希质量的一大进步。您使用的 MD5 哈希有 128 位,有数十亿个可能的哈希值。您可能会得到更好的结果的唯一方法是从您希望通过您的应用程序的数据中获取一些有代表性的样本,并在其上尝试各种算法,看看您遇到了多少冲突。

      所以在我找到不使用固定哈希算法的理由(也许是性能?)之前,我将不得不建议你坚持现有的。

      【讨论】:

        【解决方案9】:

        无论您想要一个完美的哈希函数(每个对象的计算结果相等的不同值)还是只是一个相当好的哈希函数始终是性能权衡,通常需要时间来计算一个好的哈希函数,如果您的数据集很小,您就是最好具有快速功能。最重要的(正如您的第二篇文章指出的那样)是正确性,要实现这一点,您只需要返回数组的长度即可。根据您的数据集,甚至可能没问题。如果不是(假设所有数组都一样长),您可以使用便宜的方法,例如查看第一个值和最后一个值并对它们的值进行异或运算,然后在您认为适合您的数据时添加更多复杂性。

        查看哈希函数对数据的执行情况的一种快速方法是将所有数据添加到哈希表并计算调用 Equals 函数的次数(如果经常需要对函数执行更多工作) .如果您这样做,请记住在开始时哈希表的大小需要设置为大于数据集,否则您将重新哈希数据,这将触发重新插入和更多 Equals 评估(尽管可能更现实?)

        对于某些对象(不是这个),可以通过 ToString().GetHashCode() 生成快速 HashCode,当然不是最佳的,但很有用,因为人们倾向于从 ToString() 和这正是 GetHashcode 正在寻找的内容

        琐事:我见过的最糟糕的表现是有人错误地从 GetHashCode 返回了一个常量,但使用调试器很容易发现,尤其是当您在哈希表中进行大量查找时

        【讨论】:

          【解决方案10】:

          RuntimeHelpers.GetHashCode 可能会有所帮助:

          来自 Msdn:

          作为一个散列函数 特殊类型,适用于 哈希算法和数据结构 比如哈希表。

          【讨论】:

            【解决方案11】:
            private int? hashCode;
            
            public override int GetHashCode()
            {
                if (!hashCode.HasValue)
                {
                    var hash = 0;
                    for (var i = 0; i < bytes.Length; i++)
                    {
                        hash = (hash << 4) + bytes[i];
                    }
                    hashCode = hash;
                }
                return hashCode.Value;
            }
            

            【讨论】:

              猜你喜欢
              • 2018-05-18
              • 1970-01-01
              • 1970-01-01
              • 2016-10-07
              • 2013-09-24
              • 2011-08-09
              • 2015-02-18
              • 2018-04-10
              相关资源
              最近更新 更多