为什么我的字典在C#中使用复合键表现不佳?

时间:2016-02-05 10:24:27

标签: c# performance dictionary recursion

我有一个使用递归来遍历树并更新项目的方法。

目前该方法需要很长时间来处理所有项目,所以我开始优化。其中包括使用字典而不是为每个项目执行数据库查询。

字典定义为

System.Collections.Generic.Dictionary<EffectivePermissionKey, MyData>

密钥类型定义为

private struct EffectivePermissionKey
{
  // http://blog.martindoms.com/2011/01/03/c-tip-override-equals-on-value-types-for-better-performance/
  public override bool Equals(object aObject)
  {
    if (aObject == null)
      return false;
    else
      return aObject is EffectivePermissionKey && Equals((EffectivePermissionKey)aObject);
  }

  public bool Equals(EffectivePermissionKey aObject)
  {
    return this.ID == aObject.ID && this.OrchardUserID == aObject.OrchardUserID;
  }

  public override int GetHashCode()
  { 
    // http://stackoverflow.com/a/32502294/3936440
    return unchecked(ID.GetHashCode() * 23 * 23 + OrchardUserID.GetHashCode() * 23);
  }

  public int ID;
  public int OrchardUserID;
}

当方法运行时,需要大约5000次递归才能更新所有项目。

最初在没有字典的情况下花了 100秒

使用带有int密钥的字典替换数据库查询的第一种方法花了 22秒

现在,将DB查询替换为使用上面定义的字典并进行适当的TryGetValue()调用,需要 97秒&lt; - WAT。

这里发生了什么?什么可能导致这种巨大的性能下降?

修改

起初,它看起来像是一个哈希冲突问题,所以我在EffectivePermissionKey.Equals()中添加了一个断点来验证这个方法是否被调用但是没有被调用,因此我认为没有哈希冲突。

EDIT2

现在我很困惑。我认为只有在哈希码匹配时才会调用Equals()。打印出我的密钥的哈希码和TryGetValue()中使用的密钥后,我看到这些代码匹配。然后我查看了Dictionary<>的源代码,FindEntry()中有一行看起来像这样:

if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) return i;

这意味着对于字典中的每个项目键,GetHashCode() Equals()会被调用,因为我处理字典中的所有项目,因为项目是数据库查询,而这些结果无论如何都要在字典方法之前处理。

1 个答案:

答案 0 :(得分:3)

没关系,不好意思花时间,我的方法完全错了。我告诉你为什么。

为简单起见,问题已经解决:

A -> recursion 1, DB query for permission of node A with ID = 1
  B -> recursion 2, DB query for permission of node B with ID = 2
  C -> recursion 3, DB query for permission of node C with ID = 3
    D -> recursion 4, DB query for permission of node D with ID = 4

如您所见,每个树节点有一个数据库查询。

现在有缺陷的优化方法:

Dictionary<int, PermissionData> myMap

...

DB query of all permissions and insert into myMap

...

A -> recursion 1, myMap.TryGetValue(1, out ...)
  B -> recursion 2, myMap.TryGetValue(2, out ...)
  C -> recursion 3, myMap.TryGetValue(3, out ...)
    D -> recursion 4, myMap.TryGetValue(4, out ...)

您现在看到查询已完成一次,但在每个节点上都进行了TryGetValue()调用。

在我的特定情况下,这实际上比执行单个查询要慢,因为

  • 字典包含与节点一样多的密钥,因为每个节点都有一个DB权限条目

  • 每个TryGetValue()需要/结果

    1. 创建密钥实例(包含ID和用户ID)
    2. 致电TryGetValue()
    3. 计算密钥实例的哈希值
    4. 致电Equals()

与执行5000个简单实体框架查询(SELECT * FROM table WHERE ID = ...)相比,这4个步骤执行约5000次。我不知道为什么,但这里的查询速度更快,也许编译器可以优化一些东西。

无论如何,我重写了整个事情,现在我有一个外部循环用户ID和一个内部递归遍历女巫使用带有简单的int键(节点ID)的字典。它给我照明快速的结果。整个执行现在需要大约16秒,并且通过一些调整和线程我将其降低到不到1秒。完成任务。

修改

在与同事讨论此问题后,我们得出结论,性能问题很可能是由哈希码计算中使用的素数引起的。我使用23 x 23 x 23,但它应该像17 x 23 x 23,以避免碰撞,但我不能测试这个,因为相关的代码/应用程序不再是我的责任。顺便说一下,通用解决方案可以在这里找到:https://stackoverflow.com/a/763966/3936440

修改2

如同一位同事所述,以下答案建议使用17和23,而是使用更大的素数,请参阅https://stackoverflow.com/a/38281271/3936440