Java 使用 class 作为地图键的性能

Java performance of using class as key of map

所以基本上我是在尝试创建一个映射来使用它们自己的类型对元素进行排序,然后能够使用该类型检索它们,同时一次只能容纳每种类型的一个项目。为此,我选择使用 Map<Class<? extends ParentClass>, ParentClass>.

我认为它在Java中被称为异构容器(参见this)但后来我开始考虑性能问题(因为我只是好奇,我认为我不会曾经看到过差异,但谁知道)。因此,我没有使用 ChildClass.class 来存储我的对象,而是打算在所有子类的每个 class 声明中声明一些标识符变量,但我无法强制执行它的声明,因为它需要是一个静态。

我应该继续使用.class作为地图的键吗?

谢谢。

I started thinking about performance issues (because I'm just curious, I don't think I will ever see a difference but who knows)

一般是错误;性能非常复杂,很难真正见证。 CPU 会做各种 疯狂的 技巧(这是最近出现的一些持久的黑客向量的原因,例如 spectre,以及您的普通计算机忙着把时间花在很多很多不同的事情上。这意味着 'just measure it' 不是特别可行:一个看似 'stable' 的算法(例如,循环 100,000 次的算法)在其生命周期内具有不同的性能,因此您可以'只是测量一个循环并做出假设,这里 运行s 的代码会对不同的代码产生显着的性能影响;所以你可以编写一个看起来很快的算法,如果你测量它,它很快,但无论如何对整个系统的净影响是相当负面的,因为它使其他代码 运行 变慢(创建大量寿命更长的垃圾的代码会这样做)。

换句话说,无论是对性能进行推理,还是进行一些基本测量来支持您的推理,实际上得出正确结论的可能性是多少?极低。相反,它可能会导致您得出误导性或完全错误的结论。

更一般地说,性能太难推理了,这不仅仅是我的观点,这是核心团队的观点 JDK 本身:他们不认为他们可以只查看代码并就其执行方式做出有意义的陈述。

此规则有 2 个主要例外:

  1. 算法复杂度。这是涉及 'big-O' 符号的概念,并试图衡量在根据已知变量绘制图表时的性能(根据 CPU 加载 and/or 内存要求)'charts'。例如,你的平均高效排序算法(如快速排序)具有这样的特征,即如果你针对 'time taken to sort a list' 绘制图表 'size of the input list',那么最终如果 'size of input' 列表足够大,该图表将显示就像 y = x*log(x)。问题是,y = x^2 意味着你的算法只会变慢,无论你想进行多少次微优化,对于大输入,甚至 'bigger' 比 y=x^2 (例如 y=x^3y=e^x)意味着它是无望的 - 大输入将永远(数十亿年),该算法根本不能用于大输入。无论您使用多少数据中心或进行多少调整。

可以分析算法复杂度。如果你有一份工作,你知道这份工作最终会涉及足够大的输入,因此复杂性很重要,并且你知道一种算法复杂性较低的算法,那么你只需将该代码重写为更好的算法,然后就可以放心了这将使性能提高许多数量级,无需测量任何东西或试图弄清楚系统在做什么;基础数学使这不可避免。

  1. 使用分析器和 JMH。

探查器会检查您的代码在哪里花费的时间最多。如果你有一个项目,比方说,100,000 行代码并且它没有 运行ning 像你想要的那样快,那么几乎总是大约 100 行(占全部代码的 0.1% 到 1%)负责 99 运行 它花费了 %+ 的时间。因此,仅 'start optimizing code here and there' - 99900 行代码完全没有意义 完全无关 ,您需要处理这 100 行代码,在其他地方花费的任何时间都有点像尝试通过在阿姆斯特丹的海里撒尿来提高纽约的海平面。技术上?当然可以。务实地?那是堂吉诃德级的白痴。

预测哪个代码实际上是导致性能问题的原因并不容易,因此,仅查看代码或进行一些基本测量会误导您。编写应用程序。 运行 分析器。然后根据结果采取行动。在 'less algorithmic complexity' 意义上,除了编写 'better' 算法之外,这是唯一明智的做法。

探查器只测量系统花费大部分时间的地方,而不测量花费多长时间。这充满了危险(例如,快速算法会减慢其他一切的速度),但如果你必须这样做,JVM 是一个复杂的机器,它一直在重写代码,所以 'measure how long something takes' 比你想象的要复杂得多是。因此,如果你想这样做,你不能只启动一个计时器,运行 一些代码,然后检查花费了多少时间。相反,使用 Java Microbenchmark Harness.

现在让我们进入您的实际问题

HashMap 使用以下算法:

  1. 要查找密钥,首先获取密钥并通过调用其 hashCode() 方法获取其 hashCode。
  2. 哈希图由桶组成;一个对象应该在哪个桶中取决于它的哈希码。所以,现在我们有了它,找到合适的桶。
  3. 在桶中查找右边'position',钥匙在,不在。完全没有必要去别的地方看。
  4. 如果 2 个不同的对象仍然以相同的哈希码结束(所谓的 哈希码冲突),那么只需在其中存储一个包含所有元素的链表。

结论很简单:只要几乎零碰撞,关键对象使用的hashCode方法很快,那么 所有对单个元素进行操作的 hashmap 操作本质上都是 O(1) - 换句话说,它们很快,并且即使您的地图中有数百万个项目,它们也会保持快速。这是因为无论地图中有多少项目,上述算法都采用相同数量的步骤,并且每个步骤 运行s 在常数时间内,因此整个操作在常数时间内 运行s。除了 'walk through the linked list of all different objects with the same hashcode as what we are looking for' 部分,这就是为什么过度碰撞 ARE 对性能不利。

幸运的是:

  • java.lang.Class 没有过度碰撞的特殊风险。
  • java.lang.ClasshashCode()执行速度非常快。

换句话说,使用 class 对象作为地图中的键在性能方面非常出色:算法复杂性方面,它具有 O(1) 行为,你不能得到更好的,对于细微的调整,你将不得不使用分析器,你不能只是 'wonder' 或 'ask' - 它取决于太多因素,无法直接告诉你.

你的容器是否是异构的,对它的性能特征影响为零。泛型在 运行 时甚至不可用,它们根本不会影响它。

高性能代码的关键要点

拥有快速代码库的一般方法是拥有干净代码库。在您的应用程序完成之前,您无法进行概要分析,只有到那时您才知道应该将性能工作的重点放在何处。这意味着你想要一个干净灵活的代码库:一个很容易完全重写它的一小部分的代码库。

这最好通过做与低能表现'guides'告诉你做的相反的事情。换句话说,'use an array instead of a list, it will be faster!' 看似合乎逻辑但却是相反的建议!数组远不如列表灵活;这意味着重写一些代码更难,因为代码库中不相关的(出于性能目的)部分将数据流入和流出您需要重写的部分更难修复,因为您选择了不太灵活的数据类型 'for performance reasons' .

无论哪种代码风格让您觉得易于阅读、易于测试、易于验证(在阅读它时您会意识到它做了您想要它做的事情并且不容易做任何其他事情 - 使用适当的类型而不是所有整数和字符串是提高可验证性的好方法)-这是性能最高的代码,因为这意味着适应对性能至关重要的 1% 将变得更加简单和可靠。您正在尝试避免感觉更改一件事可能会使整个纸牌屋倒塌的代码库。

那个,并了解算法的复杂性。不过那是一门大学水平的课程。

使用 类 作为地图键是很常见的。

参见示例 JTree.createDefaultRenderers():

protected void createDefaultRenderers() {
  defaultRenderersByColumnClass = new UIDefaults(8, 0.75f);

  // Objects
  defaultRenderersByColumnClass.put(Object.class, (UIDefaults.LazyValue) t -> new DefaultTableCellRenderer.UIResource());

  // Numbers
  defaultRenderersByColumnClass.put(Number.class, (UIDefaults.LazyValue) t -> new NumberRenderer());

  // Doubles and Floats
  defaultRenderersByColumnClass.put(Float.class, (UIDefaults.LazyValue) t -> new DoubleRenderer());
  defaultRenderersByColumnClass.put(Double.class, (UIDefaults.LazyValue) t -> new DoubleRenderer());

  // Dates
  defaultRenderersByColumnClass.put(Date.class, (UIDefaults.LazyValue) t -> new DateRenderer());

  // Icons and ImageIcons
  defaultRenderersByColumnClass.put(Icon.class, (UIDefaults.LazyValue) t -> new IconRenderer());
  defaultRenderersByColumnClass.put(ImageIcon.class, (UIDefaults.LazyValue) t -> new IconRenderer());

  // Booleans
  defaultRenderersByColumnClass.put(Boolean.class, (UIDefaults.LazyValue) t -> new BooleanRenderer());
}