【问题标题】:How does dead code elimination of Math.log() work in JMH sampleMath.log() 的死代码消除如何在 JMH 示例中工作
【发布时间】:2015-10-23 10:23:13
【问题描述】:

每个尝试利用 JMH 框架创建一些有意义的测试的人都会遇到 JMH 示例测试 (http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/)。 当我们浏览它们时,我们被死代码消除(JMHSample_08_DeadCode.java)卡住了。

摘录:

private double x = Math.PI;

@Benchmark
public void baseline() {
 // do nothing, this is a baseline
}

@Benchmark
public void measureWrong() {
 // This is wrong: result is not used, and the entire computation is optimized out.
 Math.log(x);
}

measureWrong() 的测量值大约为 与基线测试相同。因为返回值 Math.log() 从未使用过。因此 HotSpot 编译器 将消除死代码。好的,明白了,但是编译器如何决定可以消除 Math.log()。

当我们仔细观察测试时,我们注意到 Math.log() 是一个本地方法。 并且本机调用进入操作系统并执行相应的库。正确的? 这导致我们假设,如果本地调用的返回值没有被使用并且它们不执行 io 操作,编译器可以消除它们。

我们想知道如果 lib 驻留在操作系统中的某个位置并处理来自 java 世界的本机调用会怎样 不提供返回值,但执行 io 操作(例如日志记录)。 这些指令会被彻底清除吗?

为了证明我们的假设,我们使用简单的 JMH 测试和本地调用重构了场景。 我们编译了三个 c-native 库来执行:

  1. 返回 42
  2. 参数添加
  3. 空文件创建

正如我们在 JMH 测试中所称的那样(类似于 measureWrong() 测试),它们都没有被消除,即使是不执行 io 操作的那个也没有。 由于测试结果无法证实我们的假设。本机调用无法优化,这意味着 Math.log() 和自定义本机调用没有相同的基础。他们不是平等的本地人。也许我们在原生 lib 代码中犯了一个错误,至少应该取消 test 1 的原生调用。如果这是真的,我们将与您分享我们的代码。

所以我们进一步搜索并找到了一个术语 Intrinsics ,其中 java 代码将替换为 对应架构非常优化的代码。 java.lang.Math.log() 具有这样的内在实现。 Natives和Intrinsics之间有什么关系吗? 如果上述 Natives 和 Intrinsics 之间的关系假设成立,编译器会执行以下步骤来消除 native 调用吗?

  • 在编译时,HotSpot 会检查 Math.log() 的内部实现(在 jdk 中?)是否存在,并用该代码替换 Math.log()。
  • 然后第二次检查发生在 HotSpot 查看方法的返回值的位置。根据这个结果,HotSpot 决定完全消除 Math.log() 调用。

【问题讨论】:

  • Math.log 实际上是一个内在函数。这意味着,它是 JIT 已知的,并用一组预定义的机器指令代替。此外,jit 不会消除本机指令,因为它不能证明它们没有副作用。 Android 可能仍将其本机方法视为内在函数。

标签: java intrinsics microbenchmark jmh


【解决方案1】:

当我们仔细观察测试时,我们注意到 Math.log() 是一个本地方法。并且本机调用进入操作系统并执行相应的库。对吧?

本机调用不会转到操作系统,而是通过 JNI 转到本机库。这可能最终会进入操作系统,或者可能会进入某些用户提供的库。对于 JDK 中的本地方法,我们还可以预期一些本地调用被编译为内部函数。

这导致我们假设,如果本地调用的返回值没有被使用并且它们不执行 io 操作,编译器可以消除它们。

JVM 不会查看任意本机调用来确定它们可能具有或不具有何种副作用。这意味着本机调用确实是作为方法调用进行的(在汇编级别,您跳转到某处的外部代码,堆栈上的另一个框架等)。这也意味着 JVM 无法消除它们或它们的依赖输入。

无法优化原生调用,这意味着 Math.log() 和自定义原生调用没有相同的基础。

是的。

Natives 和 Intrinsics 之间有什么关系吗?

一些原生 JDK 方法是内在函数。但是普通的 JDK 方法也可以是内在函数。内在方法的集合也因不同的 JVM 不同而不同。

如果以上对 Natives 和 Intrinsics 关系的假设成立,编译器是否会执行以下步骤来消除 native 调用?

Math.log 函数在 C2 编译器 IR(中间表示)中转换为特殊节点。这个节点可以被优化掉,因为它没有副作用,而且它的值永远不会被使用。如果使用该值,则 JVM 知道为此节点发出专门的机器代码。

总结: 内在函数是内置在 JVM 编译器中的优化方法替换。它们可用于将任何方法(本机或其他)替换为:

  1. 专门的汇编代码
  2. 专用 IR 码
  3. 内部 JVM 方法调用
  4. 以上的组合

【讨论】:

  • 感谢您提供详细而有帮助的答案。对原生代码中可能的副作用缺乏了解解释了为什么我们的原生调用没有被消除。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-02-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-01-29
相关资源
最近更新 更多