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

Remainder operator on int causes java.util.Objects.requireNonNull?

我正在尝试通过一些内部方法获得尽可能多的性能。

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
}

在我的分析器中,我看到 java.util.Objects.requireNonNull 中有 1% CPU 支出,但我什至不这么称呼。检查字节码时,我看到了这个:

 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,那么为什么编译器会生成这一行?这是一个错误吗?或者 'normal' 行为?

(我可能会使用位掩码,但我只是好奇)

[更新]

  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

这样更符合逻辑。

看来我的问题是 'wrong' 因为它与运算符无关,而是与字段本身有关。还是不知道为什么..

   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

为什么不呢?

假设

class C {
    private final int taxos = 4;

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

c.test() 这样的调用,其中 c 声明为 C 必须 c 为 [=16 时抛出=].你的方法等同于

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

因为您只使用常量。由于 test 是非静态的,因此必须进行检查。通常,它会在访问字段或调用非静态方法时隐式完成,但您不这样做。所以需要一个明确的检查。一种可能是调用 Objects.requireNonNull.

字节码

不要忘记字节码基本上与性能无关。 javac 的任务是生成 一些 字节码,其执行与您的源代码相对应。这并不是要进行 任何 优化,因为优化后的代码通常更长且更难分析,而字节码 实际上是用于优化的源代码 即时编译器。所以 javac 应该保持简单....

表现

In my profiler I saw there is 1% CPU spend in java.util.Objects.requireNonNull

我首先要归咎于探查器。分析 Java 非常困难,您永远无法期待完美的结果。

您可能应该尝试将方法设为静态。你肯定应该阅读 this article about null checks.

首先,这是此行为的最小可重现示例:

/**
 * 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 编译器如何优化 compile-time constants

请注意,在 foo() 的字节码中,没有访问对象引用来获取 bar 的值。那是因为它是一个编译时常量,因此 JVM 可以简单地对 return 这个值执行 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_0 pushes the reference of this onto the operand stack to then get the bar field

这里编译器足够聪明,注意到 aload_0(成员函数中的 this 引用)在逻辑上不能是 null.

现在你的情况实际上是缺少编译器优化吗?

查看@maaartinus 的回答。