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 版)我得到了这个更合理的结果:
所以这更合乎逻辑。
【问题讨论】:
@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_0
将this
的引用 推入操作数堆栈,然后是该对象的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
必须 当c
是null
时抛出。你的方法相当于
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
。正如您自己所说,当c
为null
时,像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?的主要内容,如果未能解决你的问题,请参考以下文章