【问题标题】:HashSet<T> GetHashcode optimizationHashSet<T> GetHashcode优化
【发布时间】:2013-07-14 23:29:03
【问题描述】:

我在 C# 中有以下结构来表示图形边缘:

struct Edge
{
    public Edge(int leftA, int leftB, int leftC, int leftD, int rightA, int rightB, int rightC, int rightD)
    {
        LeftIdA = leftA;
        LeftIdB = leftB;
        LeftIdC = leftC;
        LeftIdD = leftD;

        RightIdA = rightA;
        RightIdB = rightB;
        RightIdC = rightC;
        RightIdD = rightD;
    }

    public readonly int LeftIdA;
    public readonly int LeftIdB;
    public readonly int LeftIdC;
    public readonly int LeftIdD;

    public readonly int RightIdA;
    public readonly int RightIdB;
    public readonly int RightIdC;
    public readonly int RightIdD;
}

并且需要在 HashSet 中存储大量(大约 500 万),这样就不会出现重复。什么是 GetHashCode 的良好实现,因此它针对速度进行了优化?

我尝试将每个 id 的 4 位存储在返回的整数中,如下所示:

    public override int GetHashCode()
    {
        int A = LeftIdA & 0xF;
        int B = LeftIdB & 0xF;
        int C = LeftIdC & 0xF;
        int D = LeftIdD & 0xF;

        int E = RightIdA & 0xF;
        int F = RightIdB & 0xF;
        int G = RightIdC & 0xF;
        int H = RightIdD & 0xF;

        int result = A;
        result = (result << 4) | B;
        result = (result << 4) | C;
        result = (result << 4) | D;
        result = (result << 4) | E;
        result = (result << 4) | F;
        result = (result << 4) | G;
        result = (result << 4) | H;

        return result;
    }

但它比将项目添加到列表要慢 80%。

【问题讨论】:

  • 如果你为你的数据集找到了 unique hash(你似乎有),而不是简单地将 Dictionary 预增长到大量项目(即两倍你期望?),看看它是否足够快(HashSet 似乎没有“预增长”方法)。
  • 这种散列策略似乎不能保证唯一性,所以不适合作为字典的键。否则,我会同意。
  • @CSJ:哈希码不需要唯一。对于任何包含超过 32 位数据的数据类型,都无法创建唯一的 32 位哈希码。
  • @Guffa:确实。但是,它不能用于选择字典键,除非它的值是唯一的。这就是为什么哈希集是合适的数据结构,而不是字典。
  • @CSJ:决定唯一性的不是哈希码,而是相等比较。 HashSet 和 Dictionary 也是如此。

标签: c# performance graph hashcode


【解决方案1】:

GetHashCode 的最佳实现是什么,以便对速度进行优化?

由于您的所有字段都是只读的,最好的办法可能是在构造函数中预先计算哈希码,然后从GetHashCode 返回。

要预先计算哈希码,您可以使用 Guffa 答案中的公式。

【讨论】:

    【解决方案2】:

    添加到HashSet 需要更长的时间,这并不是因为在 GetHashCode() 实施。事实上,这个实现看起来相当不错。 HashSet 必须在里面做各种疯狂的废话,比如设置水桶并将东西放入其中。

    性能提升在于查找哈希集中的元素。尝试将 500 万个不同的项目添加到列表和哈希集中,看看哪个容器能够更快地告诉你它是否包含特定的边缘。那时您可能愿意支付不到两倍的设置时间。

    【讨论】:

    • 好点。肯定会很慢,但是可以更接近列表吗?无论如何,添加到列表中并不能保证项目的唯一性。
    【解决方案3】:

    为了达到最佳效果,哈希码应尽可能减少冲突,即产生尽可能多的哈希码。

    尝试生成哈希码,以便使用来自所有成员的所有数据:

    public override int GetHashCode() {
      return
        LeftIdA ^ LeftIdB ^ LeftIdC ^ LeftIdD ^
        RightIdA ^ RightIdB ^ RightIdC ^ RightIdD;
    }
    

    与素数相乘会产生非常好的分布,因此您应该测试在您的情况下这是否会提供更好的性能:

    public override int GetHashCode() {
      return
        ((((((LeftIdA * 251 + LeftIdB) * 251 + LeftIdC) * 251 +
        LeftIdD) * 251 + RightIdA) * 251 + RightIdB) * 251 +
        RightIdC) * 251 + RightIdD;
    }
    

    注意:确保您还为结构提供了优化的相等比较。默认实现会使用反射来确定要比较的所有成员,因此速度很慢。

    编辑:

    我做了一些测试,通过第二个实现,我可以在大约两秒内将 500 万个项目添加到 HashSet。

    【讨论】:

    • 你的实现增加了整数溢出的问题。
    猜你喜欢
    • 1970-01-01
    • 2018-04-12
    • 2011-06-05
    • 2015-04-20
    • 1970-01-01
    • 2011-08-20
    • 1970-01-01
    • 2010-11-29
    • 2014-01-03
    相关资源
    最近更新 更多