Objects.requireNonNull是否比旧的方式效率低?

68
自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方法会导致性能损失?

22
不是。您那位自称“有见识”的同事显然并没有他/她自认为的那么有见识。无论它可能会对性能产生多少影响,它都是微不足道的,并且如果JIT检测到该方法被频繁使用,它很可能会进行内联。 - Boris the Spider
21
即时编译器将愉快地内联这些方法调用。你应该提醒你的老徒弟优化的第一条规则:不要。他应该先证明这个方法调用是导致一个重大问题的原因,然后才能强制每个人生成可读性更差的代码。 - JB Nizet
11
“OpenJDK bug 8073479” 将JDK传统的“foo.getClass()”空值检查转换为使用“Objects.requireNonNull”,声称不会影响性能,有时甚至能提高性能。 - Jeffrey Bosboom
这可能听起来很蠢,但我认为在Object类上添加一个public Object self() { return this; }会更美观。不是吗? - Jaumzera
4
http://cr.openjdk.java.net/~shade/scratch/NullChecks.java - user8389458
2
Brian Goetz在2015年对这个问题发表了评论:…未来JVM很可能会将Objects.requireNonNull内置化,这也是更喜欢它的另一个原因。 - Basil Bourque
9个回答

82

让我们来看一下Oracle JDK中requireNonNull的实现:

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

那非常简单。JVM(至少是Oracle的)包括一个优化的两阶段即时编译器,用于将字节码转换为机器代码。如果可以通过这种方式获得更好的性能,它将内联这样的微不足道的方法。
因此,不会变慢,没有任何实质性的影响,也不会在任何有意义的地方产生影响。
所以我的问题是:是否有任何证据表明使用Objects.requireNonNull方法会导致性能损失?
唯一有意义的证据将是您的代码库或设计成高度代表性的代码的性能测量结果。您可以使用任何体面的性能测试工具进行测试,但除非您的同事能指出与此方法相关的代码库中的实际性能问题(而不是合成基准),否则我倾向于认为您和他/她有更重要的事情要处理。
作为一点附带说明,我注意到你的样例方法是一个私有方法。因此,只有你团队编写的代码直接调用它。在这些情况下,你可以考虑是否需要断言而不是运行时检查。断言的优点在于根本不在“发布”代码中执行,因此比问题中的任何一种选择都要快。显然,你需要运行时检查的地方,但通常是在门卫点、公共方法等处。仅供参考。

1
还有一些让我困惑的事情:当调用 requireNonNull 版本的第二个参数是错误信息时,创建一个 String 实例是否会造成任何成本?如果是,则在 if 块中隐藏错误消息是否避免了当 == null 测试为“false”时的成本? - Bobulous
8
字符串已经存在,因为字符串字面量在类文件加载时预加载并放入字符串池中。如果您正在执行Objects.requireIfNull(blah,“literal”+ runtimeString);,则将在运行时(在底层)创建一个 StringBuilder 进行连接,即使不使用结果字符串也是如此。这可能会带来一些开销。但如果您使用字面量或对现有静态字符串的引用,则没有问题。而且不要低估JIT!它完全可以重新排序代码以避免像那样的死存储,并且不仅是内联。 - T.J. Crowder
2
@Bobulous 这个答案是完全正确的,我只想补充一下,如果你需要一个动态消息,请考虑使用Guava的checkNotNull with an errorMessageTemplate。这将消除字符串连接(如果不需要,则创建一个可变参数数组,这样更便宜,可能更容易被JIT消除)。 - maaartinus

22

就形式而言,你的同事是正确的:

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

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

  • 如果由于上述任何原因该方法未被内联,则会使用T.J. Crowder提出的参数,如果您使用连接生成错误消息。

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

另一方面,存在FreqInlineSize JVM选项,它禁止内联太大(以字节计算)的方法。该方法的字节码由它们自己计算,而不考虑调用该方法的方法大小。因此,有时将代码片段提取为独立的方法可能很有用,在requireNonNull()的示例中,已经为您进行了此提取。


6
如果您想要得到证据......那么获取它的方法是编写一个微型基准测试。 (我建议先看看Calliper项目!或者根据Boris的建议查看JMH。无论哪种方式,都不要尝试从头开始编写微型基准测试。太容易出错了。)
然而,您可以告诉您的同事两件事:
• JIT编译器很好地内联小方法调用,并且这在这种情况下很可能会发生。
• 如果它没有内联调用,则性能差异只有3到5个指令,极有可能不会造成显著差异。

3
JMH比较空值检查的方法表明,即使内联失败,Objects.requireNonNull方法所需时间也比if-null代码少不到2纳秒。而当代码被内联时,这两种方法之间似乎没有任何显著的差异。 - Bobulous

4

是的,有证据表明手动 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();
    }
}

1

Effective Java by Joshua Bloch

条款67: 谨慎进行优化

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

在计算机领域,为了追求效率而犯下的错误比其他任何原因都要多,包括盲目愚蠢。
—William A. Wulf [Wulf72]

我们应该忘记小的效率问题,大约97%的时间都不要去考虑它们:过早的优化是万恶之源。
—Donald E. Knuth [Knuth74]

在优化问题上,我们遵循两条规则:
规则1. 不要优化。
规则2(仅限专家)。不要立即优化 - 也就是说,在你拥有一个完全清晰、未经优化的解决方案之前不要这样做。
—M. A. Jackson [Jackson75]


6
这是一条不错的通用建议,但并不适合作为对这个具体问题的回答。Stack Overflow 不是一个论坛 - Tom Zych

1

你的同事很可能是错的。

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

你应该使用JDK中提供的实用程序方法。


1

嗯,不是很好。但是,是的。

,直接使用代码总是更好的,因为方法堆栈不需要被修改。

,如果VM实现具有null-check跳过或一些优化的null-checks。

,方法堆栈很容易被修改和更新(但会消耗一些时间)。


0
一般而言,易读性和可维护性应该优先于优化。
这条规则可防止那些认为自己知道编译器工作原理的人进行猜测性优化,即使他们从未尝试过编写编译器,也没有深入了解过编译器。
除非同事能证明性能损失对用户具有显著和无法承受的影响,否则他们是错的。

-4

Objects.requireNonNull更加优化,因为如果你使用它,你可以实现代码重用。 同样,在Oracle中,requireNonNull被定义为

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

所以它已经是字节码了。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接