我开始考虑性能问题(因为我只是好奇,我认为我永远不会看到差异,但谁知道呢)
通常是一个错误;表演非常复杂,很难真正见证。 CPU 会做各种 疯狂 的把戏(这就是造成一堆这些最近持续存在的黑客攻击载体的原因,例如 spectre,而您的普通计算机正忙于将时间花在许多不同的这意味着“仅测量它”并不是特别可行:一个看似“稳定”的算法(例如,循环 100,000 次的算法)在其生命周期内具有不同的性能,因此您不能只测量一个循环并做出假设,然后编写代码在这里运行会对不同的代码产生显着的性能影响;所以你可以编写一个看起来很快的算法,如果你测量它,它很快,但对整个系统的净影响是相当负面的,因为它会使其他代码运行缓慢(创建大量寿命更长的垃圾的代码会这样做)。
换句话说,无论是对性能进行推理,还是做一些基本的测量来支持你的推理,实际上是否有可能得出正确的结论?极低。相反,它可能会导致您得出误导或完全错误的结论。
更一般地说,性能实在是太难解释了,这不仅仅是我的观点,而是核心 JDK 本身的团队的观点:他们认为他们不能只看代码和对它的表现做出有意义的陈述。
此规则有 2 个主要例外:
- 算法复杂性。这是涉及“big-O”符号的概念,并尝试在针对已知变量绘制图表时测量性能(根据 CPU 负载和/或内存要求)“图表”。例如,您的平均高效排序算法(例如快速排序)具有以下特征:如果您根据“输入列表的大小”绘制“排序列表所用的时间”,那么图表最终会在“输入列表的大小”列表中足够大,看起来就像
y = x*log(x)。问题是,y = x^2 意味着你的算法将会很慢,不管你想做多少微优化,对于大输入,甚至比y=x^2 更大的东西(比如y=x^3或y=e^x) 意味着它是无望的 - 大输入将永远持续(数十亿年),该算法根本不能用于大输入。无论您使用多少数据中心或进行了多少调整。
可以分析算法复杂度。如果你有一份工作,你知道这份工作最终会涉及到足够大的输入以使复杂性变得重要,并且你知道算法复杂度较低的算法,那么你可以将该代码重写为更好的算法,然后安全可靠地知道这将使性能提高许多数量级,无需测量任何东西或试图弄清楚系统在后台做什么;基础数学使这不可避免。
- 使用分析器和 JMH。
分析器检查您的代码在哪里花费的时间最多。如果你有一个项目,比如说,100,000 行代码,但它的运行速度没有你想要的那么快,那么几乎总是大约 100 行(整体的 0.1 到 1%)负责 99% 以上的时间采取运行它。因此,仅仅“开始在这里和那里优化代码”绝对没有意义 - 99900 行代码完全无关,您需要处理这 100 行代码,而在其他地方花费的任何时间都有点像尝试通过在阿姆斯特丹的海里撒尿来提高纽约的海平面。技术上?当然可以。务实?那是堂吉诃德级别的白痴。
要预测哪些代码实际上是导致性能问题的原因并不容易,因此,仅查看代码或进行一些基本测量会误导您。编写应用程序。运行探查器。然后根据结果采取行动。除了编写“更好的”算法之外,在“算法复杂度更低”的意义上,这是唯一明智的做法。
分析器只是衡量系统花费大部分时间的地方,而不是衡量某件事需要多长时间。这充满了危险(例如,可以减慢一切速度的快速算法),但如果你必须这样做,JVM 是一台复杂的机器,它一直在重写代码,所以“测量某件事需要多长时间”比你复杂得多认为是。因此,如果你想这样做,你不能只启动一个计时器,运行一些代码,然后检查花费了多少时间。请改用Java Microbenchmark Harness。
现在让我们来回答您的实际问题
HashMaps 使用以下算法:
- 要查找密钥,首先获取密钥并通过调用其
hashCode() 方法获取其hashCode。
- hashmap 由桶组成;对象应该在哪个桶中取决于它的哈希码。所以,现在我们有了它,找到合适的存储桶。
- 在存储桶中查找正确的“位置”,密钥是否存在。完全没有必要去其他地方寻找。
- 如果 2 个不同的对象最终得到相同的哈希码(所谓的哈希码冲突),那么只需在其中存储一个包含所有元素的链表。
因此结论很简单:只要冲突几乎为零,并且 key 对象使用的hashCode 方法很快,那么所有对单个操作的hashmap 操作元素本质上是 O(1) - 换句话说,即使您的地图中有数百万个项目,它们也很快,并且仍然很快。那是因为上面的算法不管map有多少个item都走相同的步数,而且每一步运行的时间是固定的,所以整个操作运行的时间是固定的。除了“遍历所有具有与我们正在寻找的相同哈希码的不同对象的链表”部分,这就是为什么过多的冲突是对性能不利的原因。
幸运的是:
-
java.lang.Class 没有过度碰撞的特别风险。
-
java.lang.Class 的 hashCode() 实现速度非常快。
换句话说,使用类对象作为映射中的键在性能方面非常出色:算法复杂性方面,它具有O(1) 行为,你无法得到任何更好的东西,而对于小的调整内容,你'将不得不使用分析器,你不能只是'想知道'或'询问' - 这取决于太多的因素才能直接告诉你。
您的容器是否是异构的,对它的性能特征的影响为零。泛型甚至在运行时都不可用,它们根本不会影响它。
高性能代码的关键要点
拥有快速代码库的一般方法是拥有干净代码库。在您的应用程序完成之前,您无法进行概要分析,只有这样您才能知道将性能工作重点放在哪里。这意味着您需要一个干净且灵活的代码库:可以轻松地完全重写其中的一小部分。
最好与愚蠢的表演“指南”告诉你的相反。换句话说,“使用数组而不是列表,它会更快!”似乎合乎逻辑,但却是相反的建议!数组远不如列表灵活;这意味着重写一些代码更难,因为代码库中不相关的(出于性能目的)部分将数据流入和流出您需要重写的部分更难修复,因为您“出于性能原因”选择了不太灵活的数据类型.
无论哪种风格的代码让您觉得易于阅读、易于测试、易于验证(在阅读时您会意识到它做了您想做的事情并且不会轻易做任何其他事情 - 使用正确的类型而不是all-ints-and-strings 是提高可验证性的好方法) - 这是性能最高的代码,因为这意味着调整性能关键的 1% 将变得更加简单和可靠。您试图避免使用感觉就像更改一件事可能会使整个纸牌屋倒塌的代码库。
那,并了解算法的复杂性。不过,那是一门大学水平的课程。