JVM或JIT是否能够减少重复的方法调用



我有一个函数,它检查映射是否有键(map.get(key) != null(的某个值,然后返回该值,或者创建新值
我想知道,当给定键的值存在时,是否有JVM或JIT魔术不必进行2次映射查找?

Value someValue = map.get(key) != null ? map.get(key) : new Value();

根据我的基准,似乎无法进行优化,因为它明显比我们有一个局部变量来保持值慢:

@Benchmark
public String duplicateCall() {
return map.get(1) != null ? map.get(1) : DEFAULT;
}
@Benchmark
public String nonDuplicateCall() {
final String s = map.get(1);
return s != null ? s : DEFAULT;
}

结果:

Benchmark                                  Mode  Cnt       Score       Error   Units
duplicateCall     thrpt    5  634001.515 ± 69181.631  ops/ms
nonDuplicateCall  thrpt    5  869980.580 ± 66572.021  ops/ms

只有当优化器能够证明这两个操作是幂等的时,消除重复的方法调用才能起作用,这需要查看方法的实际实现代码。

您假设map.get(1)的两次出现会起到相同的作用,这取决于几个前提,JVM不能想当然。

  • 您在同一个对象实例上调用该方法。即使在这个具有相邻调用的简单代码中,优化器也不能假设这一点,而不知道get实际做了什么。如果get更改了map引用,则此假设将无效。

  • 你传递的是同一把钥匙。您的代码表明我们讨论的是Map<Integer, String>,因此该表达式受自动装箱约束。您实际上是在将Integer.valueOf(1)传递给get,并为另一个get调用再次传递Integer.valueOf(1)
    auto-boxing/Integer.valueOf(int)的特定契约允许用一个替代另一个,无论实现看起来如何,问题是优化器是否会知道并利用这一点。

  • 这种方法没有副作用。虽然可以合理地假设Mapget实现不会修改映射,但它可能包含日志记录或基准测试语句,这些语句在默认情况下无效,但可以根据某些运行时状态进行激活。如果它有这样的语句,那么要证明它们的无效性,就需要预测运行时状态。

由于消除冗余只有在知道实现代码的情况下才能起作用,因此只有在满足内联的先决条件时(例如,调用总是在同一个实现中结束(,它才能在内联代码后起作用。然后,将应用诸如公共子表达式消除之类的优化。

此优化的有效性取决于实际的实现代码。对于像Map.of()Collections.emptyMap()这样的空映射以及像Map.of(1, "foo")Collections.singletonMap(1, "foo")这样的单例映射,这可能工作得很好。但对于TreeMapHashMap的映射,实际实现过于复杂,无法假设可以完全消除冗余评估。重要的是要记住,这还需要内联密钥的hashCodeequals(或compareTo(实现,以证明它们的幂等性。对于Integer密钥不是问题,但对于其他密钥类型可能是问题。

为了说明这一点,我们正在讨论HashMap:的类似实现

public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}

优化器必须将此代码内联两次(由于代码大小阈值的原因,不内联的可能性很高(,然后证明这两次发生的情况相同,并且没有副作用…


正如评论中已经提到的,如果您想要简洁的代码并避免重复的查找操作,您可以简单地使用map.getOrDefault(1, DEFAULT)


和并发或同步的映射无论如何都不在游戏中,除非优化器能够证明它们从未被其他线程看到过。