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

Posted

技术标签:

【中文标题】int 上的余数运算符导致 java.util.Objects.requireNonNull?【英文标题】:Remainder operator on int causes java.util.Objects.requireNonNull? 【发布时间】:2020-05-19 08:16:10 【问题描述】:

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

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,那么为什么编译器会生成这一行呢?它是一个错误吗?还是“正常”行为?

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

[更新]

    运营商似乎与它无关(见下面的答案)

    使用 eclipse 编译器(4.10 版)我得到了这个更合理的结果:

public getParent(I)I throws java/io/IOException L0 第 77 行 L0 加载 1 ICONST_4 IREM 存储 2 L1 LINENUMBER 78 L

所以这更合乎逻辑。

【问题讨论】:

@Lino 当然,但这与第 70 行并不真正相关,原因是 INVOKESTATIC 你使用什么编译器?普通javac 不会生成这个。 你使用什么编译器? Java版本,Openjdk/Oracle/等?编辑:哎呀,@apangin 更快,抱歉 从 Intellij 2019.3 编译,在 ubuntu 64 位上使用 java 11, openjdk version "11.0.6" 2020-01-14 【参考方案1】:

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

/**
 * 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 可以简单地执行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引用 推入操作数堆栈,然后是该对象的get the bar field。

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

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

请参阅@maaartinus 答案。

【讨论】:

【参考方案2】:

为什么不呢?

假设

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 必须cnull 时抛出。你的方法相当于

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

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

字节码

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

表现

在我的分析器中,我看到java.util.Objects.requireNonNull 有 1% 的 CPU 开销

我会先责怪分析器。分析 Java 非常困难,您永远无法期待完美的结果。

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

【讨论】:

感谢@maaartinus 的富有洞察力的回答。我一定会阅读您的链接文章。 “测试是非静态的,必须进行检查” 实际上,没有理由测试this是否是非null。正如您自己所说,当cnull 时,像c.test() 这样的调用必须失败,并且必须立即失败,而不是进入方法。所以在test() 内,this 永远不能是null(否则会出现 JVM 错误)。所以不需要检查。实际的修复应该是将字段taxos 更改为static,因为在每个实例中为编译时常量保留内存是没有意义的。那么test()是不是static就无关紧要了。【参考方案3】:

看来我的问题是“错误的”,因为它与操作员无关,而是与字段本身有关。还是不知道为什么..

   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

【讨论】:

编译器真的会害怕this引用null吗?这可能吗? 不,这没有任何意义,除非编译器以某种方式将该字段编译为Integer,这是自动装箱的结果? ALOAD 0 没有引用 this 吗?所以编译器添加一个空检查是有意义的(不是真的) 所以编译器实际上是在为this添加一个空检查?太好了:/ 我将尝试使用命令行javac 制作一段最少的代码,以便明天进行验证;如果这也显示出这种行为,我认为这可能是 javac-bug?

以上是关于int 上的余数运算符导致 java.util.Objects.requireNonNull?的主要内容,如果未能解决你的问题,请参考以下文章

在编程里28÷6的余数该指令运行的结果是?

取余数运算

取余数运算

为啥 operator% 被称为“模数”运算符而不是“余数”运算符?

%在java中啥意思

java运算符