整数的余数运算符导致java.util.Objects.requireNonNull?

12

我试图从一些内部方法中尽可能地获得性能。

Java 代码如下:

List<DirectoryTaxonomyWriter> writers = Lists.newArrayList();
private final int taxos = 4;

[...]

@Override
public int getParent(final int globalOrdinal) throws IOException {
    final int bin = globalOrdinal % this.taxos;
    final int ordinalInBin = globalOrdinal / this.taxos;
    return this.writers.get(bin).getParent(ordinalInBin) * this.taxos + bin; //global parent
}

在我的性能分析器中,我看到有1%的CPU时间花费在java.util.Objects.requireNonNull上,但我甚至没有调用它。在检查字节码时,我看到了这个:

 public getParent(I)I throws java/io/IOException 
   L0
    LINENUMBER 70 L0
    ILOAD 1
    ALOAD 0
    INVOKESTATIC java/util/Objects.requireNonNull (Ljava/lang/Object;)Ljava/lang/Object;
    POP
    BIPUSH 8
    IREM
    ISTORE 2

因此,编译器生成了这个(无用?)的检查。我正在处理原始数据类型,它们无论如何都不可能为 null,那么为什么编译器会生成这行代码呢?这是一个错误吗?还是“正常”的行为?

(我可能会通过位掩码解决问题,但我只是好奇)

[更新]

  1. 操作符似乎与此无关(请参见下面的答案)

  2. 使用Eclipse编译器(版本4.10),我得到了这个更合理的结果:

    public getParent(I)I throws java/io/IOException 
       L0
        LINENUMBER 77 L0
        ILOAD 1
        ICONST_4
        IREM
        ISTORE 2
       L1
        LINENUMBER 78 L

因此这更加合乎逻辑。


@Lino 当然,但这与导致“INVOKESTATIC”的第70行不是真正相关的。 - Rob Audenaerde
你使用哪个编译器?普通的javac不会生成这个。 - apangin
你使用哪个编译器?Java版本,Openjdk/Oracle等?编辑:糟糕,@apangin更快了,抱歉。 - lugiorgi
1
它是使用Java 11在Ubuntu 64位操作系统上编译的Intellij 2019.3版本,使用的是openjdk version "11.0.6" 2020-01-14 - Rob Audenaerde
3个回答

3

为什么不呢?

假设

class C {
    private final int taxos = 4;

    public int test() {
        final int a = 7;
        final int b = this.taxos;
        return a % b;
    }
}

在 IT 技术领域,如果声明变量cC 类型,那么对其使用 c.test() 这样的调用时,必须cnull 的情况下抛出异常。你的方法等同于:

    public int test() {
        return 3; // `7 % 4`
    }

由于您只使用常量,因此需要检查非静态的 test 常量。通常情况下,当访问字段或调用非静态方法时,会隐式地执行此检查,但是这里没有进行。因此需要进行显式检查。一种可能性是调用 Objects.requireNonNull

字节码

请记住,字节码对性能基本上是无关紧要的。 javac 的任务是生成与您的源代码相对应的某些字节码。它的作用不是进行任何优化,因为优化后的代码通常更长,更难分析,而字节码实际上是优化 JIT 编译器的源代码。因此,期望 javac 保持简单....

性能

在我的分析器中,我看到有 1% 的 CPU 时间花费在 java.util.Objects.requireNonNull

首先要怀疑分析器。Java 的分析非常困难,你永远不能期望得到完美的结果。

您可能应该尝试将方法设为静态。您肯定应该阅读关于空指针检查的这篇文章


1
感谢@maaartinus提供的深入见解。我一定会阅读您提供的链接文章。 - Rob Audenaerde
1
“由于测试不是静态的,因此必须进行检查。” 实际上,没有理由测试this是否非null。正如您自己所说,当cnull时,像c.test()这样的调用必须失败,并且它必须立即失败,而不是进入该方法。因此,在test()中,this永远不可能为null(否则将存在JVM错误)。因此无需检查。实际的修复应该是将字段taxos更改为static,因为在每个实例中为编译时常量保留内存并没有意义。然后,test()是否为static无关紧要。 - Holger

2

看起来我的问题是“错误的”,因为它与运营商无关,而是与字段本身有关。仍然不知道为什么。

   public int test() {
        final int a = 7;
        final int b = this.taxos;
        return a % b;
    }

这将转化为:

  public test()I
   L0
    LINENUMBER 51 L0
    BIPUSH 7
    ISTORE 1
   L1
    LINENUMBER 52 L1
    ALOAD 0
    INVOKESTATIC java/util/Objects.requireNonNull (Ljava/lang/Object;)Ljava/lang/Object;
    POP
    ICONST_4
    ISTORE 2
   L2
    LINENUMBER 53 L2
    BIPUSH 7
    ILOAD 2
    IREM
    IRETURN

1
编译器会不会真的害怕this引用了null?这可能吗? - atalantus
1
不,除非编译器将该字段编译为“整数”,否则这毫无意义。 这是自动装箱的结果吗? - Rob Audenaerde
1
ALOAD 0 不是引用 this 吗?所以编译器添加一个 nullcheck 是有道理的(虽然不是真的)。 - Lino
嗯,以一种奇怪的方式来说,这确实有道理。在实例方法调用时,局部变量0始终用于传递对正在调用实例方法的对象的引用(在Java编程语言中为this)。随后,任何参数都将传递到从局部变量1开始的连续局部变量中。 - Rob Audenaerde
1
那么编译器实际上为this添加了一个空检查?太好了 :/ - Rob Audenaerde
1
我会尝试使用命令行javac编写一个最小的代码片段来验证明天;如果这也显示出这种行为,我认为这可能是一个javac-bug? - Rob Audenaerde

2
首先,这是一个最小可复现的示例,展示了这种行为:
/**
 * OS:              Windows 10 64x
 * javac version:   13.0.1
 */
public class Test {
    private final int bar = 5;

    /**
     * public int foo();
     *   Code:
     *     0: iconst_5
     *     1: ireturn
     */
    public int foo() {
        return bar;
    }

    /**
     * public int foo2();
     *   Code:
     *     0: aload_0
     *     1: invokestatic  #13     // Method java/util/Objects.requireNonNull:(Ljava/lang/Object;)Ljava/lang/Object;
     *     4: pop
     *     5: iconst_5
     *     6: ireturn
     */
    public int foo2() {
        return this.bar;
    }
}

这个行为是因为Java编译器如何优化编译时常量
请注意,在foo()的字节码中,没有访问任何对象引用来获取bar的值。这是因为它是一个编译时常量,因此JVM可以简单地执行iconst_5操作来返回该值。
当将bar改为非编译时常量(通过删除final关键字或在构造函数内部而不是声明时初始化),您会得到:
/**
 * OS:              Windows 10 64x
 * javac version:   13.0.1
 */
public class Test2 {
    private int bar = 5;

    /**
     * public int foo();
     *   Code:
     *     0: aload_0
     *     1: getfield      #7
     *     4: ireturn
     */
    public int foo() {
        return bar;
    }

    /**
     * public int foo2();
     *   Code:
     *     0: aload_0
     *     1: getfield      #7
     *     4: ireturn
     */
    public int foo2() {
        return this.bar;
    }
}

在这里,aload_0this引用推送到操作数栈上,以便 获取此对象的bar字段
编译器足够聪明,注意到在成员函数中 aload_0(即 this 引用)逻辑上不可能为 null
现在你的情况实际上是缺少编译器优化吗?
请参见 @maaartinus 的答案。

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