为什么一个 Java 字节码方法中使用的局部变量数量不是最经济的?

Why is the number of local variables used in a Java bytecode method not the most economical?

我有一段简单的 Java 代码:

public static void main(String[] args) {
    String testStr = "test";
    String rst = testStr + 1 + "a" + "pig" + 2;
    System.out.println(rst);
}

使用 Eclipse Java 编译器编译它,并使用 AsmTools 检查字节码。它显示:

方法中有3个局部变量。参数在插槽 0 中,插槽 1 和 2 应该由代码使用。但我认为 2 个局部变量就足够了——索引 0 无论如何都是参数,代码只需要一个变量。

为了看看我的想法是否正确,我编辑了文本字节码,将局部变量的个数减少到2个,并调整了一些相关指令:

我用 AsmTools 重新编译了它,它工作正常!

那么为什么 Javac 或 Eclipse 编译器不进行这种优化以使用最少的局部变量?

只是因为 Java 从即时编译器中获得了性能。

您在 Java 源代码中所做的,甚至 class 文件中显示的内容都不是在运行时启用性能的原因。当然你不应该忽略那部分,但只是在不犯“愚蠢的错误”的意义上。

意思是:jvm 在运行时决定一个方法是否值得转换成(高度优化!)机器代码。如果 jvm 决定“不值得优化”,为什么要通过在其中进行大量优化来使 javac 变得更复杂和更慢?另外:传入的字节代码越简单和基本,JIT 就越容易分析和改进该输入!

有几个原因。首先,它不是性能所必需的。 JVM 已经在运行时进行了优化,因此没有必要向编译器添加冗余的复杂性。

然而,这里没有人提到的另一个主要原因是调试。使字节码尽可能接近原始源代码可以更容易调试。

字节码验证也有问题。您知道 Jave 中的每个变量都必须先定义才能使用。如果将变量 x 和变量 y 合并在一起,顺序为 "define x, use x, use y" 字节码验证程序应将其检测为错误,但在合并这两个变量后将无法再检测到。

作为优化,最好留给即时编译器,它可以决定它想要共享哪些变量space。

好吧,你做了只是在曾经是两个完全独立的本地人之间建立了错误的依赖关系。这意味着 JIT 编译器要么需要更多 complex/slower 来解开更改并返回到原始字节码,要么在它可以进行的优化类型上受到限制。

请记住,Java 编译器在您的开发(或构建)机器上运行一次。 JIT 编译器知道它 运行 所在的硬件(和软件)。 Java 编译器需要创建简单、直接的代码,便于 JIT 处理和优化(或在某些情况下解释)。几乎没有理由过度优化字节码本身——您可能会减少可执行文件大小的几个字节,但为什么要这么麻烦,尤其是如果结果会是 CPU 效率较低的代码或更长的 JIT 编译时间?

我现在没有进行实际测试的环境,但我很确定 JIT 会从两个字节码中生成相同的可执行代码(它在 .NET 中确实如此,这在许多方式相似)。

java 的承诺是相同的代码可以 运行 在多个系统上。 Java 可以从一开始就优化其字节码。但它宁愿等到所有事实都知道了。

  • 硬件: 相同的字节码可以 运行 在 raspberry pi 或多核 unix 上 服务器64GB。
  • 用法:有些函数很少被调用,有些函数 每秒调用几次。
  • 灵活性: 将来字节码可以 运行 在不同的 JVM 上,这提供了新的优化。 (JDK x ?)

因此,通过推迟决策,可以根据所有这些变量更好地重组和微调字节码。

结论:不要rename/move/eliminate变量只是为了让代码更快。


为什么用法如此重要:

Java 跟踪哪些方法最常被调用,哪些流程在代码中最常被遵循。

一个可能的优化是"Method inlining",这意味着整个方法不仅被重组而且合并在一起。一旦将方法合并在一起,您就可以处理更大的代码块,并进行更好的优化。您实际上可以进一步消除变量,在整个流程中重复使用它们。