原始类型 HashSet 或 HashMap 比 Object 快 10 倍?

Primitive type HashSet or HashMap 10 times faster than Object?

更新: 这应该算是flutter channel stable v1.9.1+hotfix.4的bug。当切换到主通道时,这是固定的,int 和 Object Sets 具有相同数量级的性能。

一项基准测试表明 HashSet<int> 的执行速度平均比 HashSet<Object> 快 10 倍。 这种行为的原因是什么?

当我以一种我期望更高效的方式更改库的内部结构时,我的性能大幅下降(超过一个数量级)。

问题从 HashSet<int> 变为 HashSet<MyClass>,我在 运行 HashSet 中的简单基准将相同值相加 1000 次时确认了这一点。结果是 HashSet<int> 的执行速度平均比 HashSet<Object>.

快 10 倍

从代码设计的角度有什么建议吗?使用 HashSet<MyClass> 而不是使用将实例与 int 关联的其他数据结构时,代码看起来更清晰。这种性能下降很重要,因为它是库的核心。我发现保持程序高效的唯一方法是使用标识 MyClass 的整数来操作所有内容。

基准可以在这里找到: https://gist.github.com/icatalud/dc28bd3bdd7c13b39c308b7abb9a9d8c

添加到 Set 的函数如下:

plainAddSet<T>(T obj, [int n = 1000]) {
  var s = Set<T>();
  for (var i = 0; i < n; i++) {
    s.add(obj);
  }
}

更新: 运行 更多测试让我找到了性能下降的根本原因。 hashCode getter 非常慢。下面class如果不重写hashCode方法,比Object.hashCode方法耗时要长20倍,甚至慢。否则它比一个 int Set 稍微多一点。

class Identifiable {
  static int lastId = 0;
  int id;
  Identifiable() : id = lastId++;

  // Not overriding this getter, causes the benchmark to take 20 times longer.
  int get hashCode => id;

  bool operator ==(other) =>
      other.runtimeType == runtimeType &&
      hashCode == other.hashCode;
}

更新 2: 尽管如此,理解为什么覆盖 == 运算符并从那里访问 hashCode 比保留对象默认实现慢三倍(这比 int 慢 10 倍以上)。

class Identifiable {
  bool operator ==(other) =>
      other.runtimeType == runtimeType &&
      other.hashCode == hashCode;
}

更新 3: 在 dart SDK 存储库中有关于此的 open issue。它已经存在 4 年多了。

我已经用不同的 class 实现进行了测试,可以看到等待时间是针对 hashCode 的。这是有道理的,因为整数的 hashCode 与它所代表的整数相同(如果你考虑一下就有意义)。

Object()的hashCode比较复杂,需要外部调用一些我不知道的代码。

如果我创建自己的 class 并用 "always return 5" 之类的东西覆盖 hashCode 属性 然后我得到与整数相同的速度。

由于每次向 Map 或 Set 添加内容时都会使用 hashCode(和 ==),因此尽可能保持这两个函数的效率很重要。

更新 2 的答案

Object() hashCode 属性 之所以慢是因为它比整数 hashCode 做的事情多。我检查了源代码,这是它执行的步骤:

如您所见,这比仅获取整数本身的值并将其用作散列要复杂得多。正如您在代码中还可以看到的,甚至还使用了随机生成器。

我应该补充一点,Hash 被缓存了,所以这只是第一次调用 hashCode,它真的很慢。但是正如您所看到的,缓存版本仍然是一个外部调用,它比仅仅获取一个整数变量更昂贵。

"Proposing a workaround to this issue"

更新

这在 flutter channel stable v1.9.1+hotfix.4 中发生,但在 master 中已修复。应该没有必要在下一个 Flutter 版本中实现。

如果你现在不能接受标准 hashCode 方法的性能,我认为唯一的另一种方法是像这样手动缓存它:

class ObjectWithCachedHash {
  int _hashCodeCache;

  @override
  int get hashCode => _hashCodeCache ??= super.hashCode;
}

仅当 hashCode 不依赖于对象内的任何值时才应执行此操作(例如,纯粹基于实例的标准 hashCode 实现)。

更新 link 发布

缓慢的 hashCode 问题是一个已知问题(感谢 Ignacio Catalan 指出):

https://github.com/dart-lang/sdk/issues/24206

应该注意的是,在这个 Whosebug 问题中发现的性能问题更可能与最近修复的 master 回归有关:

https://github.com/dart-lang/sdk/issues/24206#issuecomment-539448012

但是这个回归的修复只会修复 x64 的问题,所以 32 位架构(以及所有 ARM 架构)仍然会受到性能问题的影响(这是另一个与回归)。

因此,对于 32 位和 ARM 用户的建议是,如果您的代码依赖于 Object.hashCode 并且要进行大量比较(例如,使用 Map 或 Set 的大量比较),则在本地缓存 hashCode 值对象)。