Objects.requireOnNull的效率是否低于旧方法



自从JDK 7以来,我一直很高兴地使用它引入的方法来拒绝传递给无法接受null值的方法:

private void someMethod(SomeType pointer, SomeType anotherPointer) {
Objects.requireNonNull(pointer, "pointer cannot be null!");
Objects.requireNonNull(anotherPointer, "anotherPointer cannot be null!");
// Rest of method
}

我认为这种方法可以生成非常整洁、易于阅读的代码,我正试图鼓励同事们使用它。但一位(特别有知识的)同事对此表示反对,并表示旧方法更有效:

private void someMethod(SomeType pointer, SomeType anotherPointer) {
if (pointer == null) {
throw new NullPointerException("pointer cannot be null!");
}
if (anotherPointer == null) {
throw new NullPointerException("anotherPointer cannot be null!");
}
// Rest of method
}

他说,调用requireNonNull涉及到在JVM调用堆栈上放置另一个方法,这将导致比简单的== null检查更差的性能。

因此,我的问题是:是否有证据表明使用Objects.requireNonNull方法会导致性能损失?

让我们看看requireNonNull在Oracle的JDK:中的实现

public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}

所以这是非常简单的。JVM(无论如何都是Oracle的)包括一个优化的两阶段实时编译器,用于将字节码转换为机器代码。如果这样可以获得更好的性能,它将内联像这样的琐碎方法。

所以不,不太可能更慢,没有任何意义,没有任何重要的地方。

那么,我的问题是:是否有证据表明使用Objects.requireNonNull方法会导致性能损失?

唯一重要的证据是您的代码库的性能测量,或设计为具有高度代表性的代码的性能测量。您可以使用任何像样的性能测试工具对此进行测试,但除非您的同事能够指出与此方法相关的代码库中性能问题的真实例子(而不是合成基准),我倾向于认为你和他/她还有更大的事情要做。


顺便说一句,我注意到您的示例方法是private方法。所以,只有您的团队正在编写的代码才能直接调用它。在这些情况下,您可能会考虑是否有用于断言而不是运行时检查的用例。断言的优点是根本不在"已发布"的代码中执行,因此在您的问题中比任何一种选择都快。显然,有些地方需要运行时检查,但这些检查通常在把关点、公共方法等处。只是FWIW。

从形式上讲,您的同事是对的:

  • 如果someMethod()或相应的跟踪不够热,则解释字节码,并创建额外的堆栈帧

  • 如果从热点在深度的第9级调用someMethod(),则由于MaxInlineLevelJVM选项,requireNonNull()调用不应内联

  • 如果由于上述任何原因,该方法没有内联,那么T.J.Crowder的论点就会发挥作用,如果您使用串联来生成错误消息

  • 即使requireNonNull()是内联的,JVM也会浪费执行此操作的时间和空间。

另一方面,还有FreqInlineSizeJVM选项,它禁止内联太大(在字节码中)的方法。该方法的字节码是自己计数的,而不计算该方法中调用的方法的大小。因此,将代码片段提取到独立的方法中有时可能很有用,在requireNonNull()的示例中,这种提取已经为您准备好了。

如果您想要证据。。。那么获得它的方法就是编写一个微基准。

(我建议先看看Calliper项目!或者JMH…根据Boris的建议。无论哪种方式,都不要试图从头开始编写微基准。有太多方法会出错。)

然而,你可以告诉你的同事两件事:

  • JIT编译器在内联小方法调用方面做得很好,在这种情况下很可能会发生这种情况。

  • 如果它没有内联调用,那么性能的差异可能只有3到5条指令,而且它不太可能产生显著的差异。

是的,有证据表明手动null检查和Objects.requireNonNull()检查之间的差异可以忽略不计。OpenJDK委员会委员Aleksey Shipilev创建了基准测试代码,在修复JDK-8073479时证明了这一点,以下是他的结论和性能数据:

TL;DR: Fear not, my little friends, use Objects.requireNonNull.
Stop using these obfuscating Object.getClass() checks,
those rely on non-related intrinsic performance, potentially
not available everywhere.
Runs are done on i5-4210U, 1.7 GHz, Linux x86_64, JDK 8u40 EA.
The explanations are derived from studying the generated code
("-prof perfasm" is your friend here), the disassembly is skipped
for brevity.

Out of box, C2 compiled:
Benchmark                  Mode  Cnt  Score   Error  Units
NullChecks.branch          avgt   25  0.588 ± 0.015  ns/op
NullChecks.objectGetClass  avgt   25  0.594 ± 0.009  ns/op
NullChecks.objectsNonNull  avgt   25  0.598 ± 0.014  ns/op
Object.getClass() is intrinsified.
Objects.requireNonNull is perfectly inlined.

其中branchobjectGetClassobjectsNonNull定义如下:

@Benchmark
public void objectGetClass() {
o.getClass();
}
@Benchmark
public void objectsNonNull() {
Objects.requireNonNull(o);
}
@Benchmark
public void branch() {
if (o == null)  {
throw new NullPointerException();
}
}

您的同事很可能错了

JVM非常智能,很可能会内联Objects.requireNonNull(...)方法。性能值得怀疑,但肯定会有比这更严重的优化。

您应该使用JDK中的实用程序方法。

约书亚·布洛赫的有效Java

第67项:明智地优化

关于优化有三句格言,每个人都应该知道:

以效率的名义犯下的计算罪(不一定能实现)比任何其他原因都多,包括盲目的愚蠢
--William A.Wulf[Wulf72]

我们应该忘记小效率,比如说97%的时间:过早优化是万恶之源
--Donald E.Knuth[Knuth74]

在优化问题上,我们遵循两条规则:
规则1。不要这么做。
规则2(仅适用于专家)。现在还不要做——也就是说,直到你有一个完全清晰且未优化的解决方案
--M。A.Jackson[Jackson75]

嗯,不。但是,是的。

,直接代码总是更好的,因为不需要触摸方法堆栈。

,如果VM实现有空检查跳过或一些优化的空检查。

Meh,方法堆栈非常轻,可以修改和更新(但它会消耗一些时间)。

一般来说,可读性和可维护性应该胜过优化。

这条规则可以防止那些认为自己知道编译器如何工作的人进行推测性优化,即使他们从未尝试过编写编译器,也从未查看过编译器内部。

除非你的同事证明性能损失是明显的,并且对用户来说是站不住脚的,否则他们是错的。

Objects.requireOnNull得到了更好的优化,就好像你是代码的可重用性。同样在oracle中requireNonNull si被定义为

public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
} 

所以它已经在字节码中了。

最新更新