我应该停止在 Java 中使用局部变量吗?

Pie*_*rre 1 java optimization bytecode compilation

我有这两个 Java 和 C++ 代码,它们应该做同样的事情。

我的直觉是, 和 的目标代码的大小(以及内容)将R1相同R2。C++ 就是这种情况(如果编译时没有 4 个字节的差异-O1)。Java 字节码有更大的差异(R2更长),这让我感到惊讶。

也许我没有看到正确的东西,我的问题可能不相关,但是Java字节码与源代码如此“接近”是正常的吗?这是否意味着它总是更“高效”/“优化” “将所有内容写在一行中而不是使用局部变量?

C++

int A(int a) { return 0; }
int B(int b) { return 0; }
int C(int c) { return 0; }
int D(int d) { return 0; }

int R1() {
  return  A(B(C(3)+D(3)));
}

int R2() {
  int d = D(3);
  int c = C(3);
  int b = B(c + d);
  return A(b);
}

// Then R1() and R2() are called in the main()
Run Code Online (Sandbox Code Playgroud)

爪哇

class MyClass {
  static int A(int a) { return 0; }
  static int B(int b) { return 0; }
  static int C(int c) { return 0; }
  static int D(int d) { return 0; }
  
  static int R1() {
    return  A(B(C(3)+D(3)));
  }
  
  static int R2() {
    int d = D(3);
    int c = C(3);
    int b = B(c + d);
    
    return A(b);
  }

  // Then R1 and R2 are called in the Main()
}
Run Code Online (Sandbox Code Playgroud)

当我编译它们(g++ -O1版本 9.4 和javac版本 11.0.17)并反汇编R1和R2时,我得到:

C++ ( g++ -O1 prog.cpp)

R1:
  1251: f3 0f 1e fa             endbr64 
  1255: 53                      push   %rbx
  1256: bf 03 00 00 00          mov    $0x3,%edi
  125b: e8 9d ff ff ff          callq  11fd <_Z1Ci>
  1260: 89 c3                   mov    %eax,%ebx
  1262: bf 03 00 00 00          mov    $0x3,%edi
  1267: e8 bb ff ff ff          callq  1227 <_Z1Di>
  126c: 8d 3c 03                lea    (%rbx,%rax,1),%edi
  126f: e8 5f ff ff ff          callq  11d3 <_Z1Bi>
  1274: 89 c7                   mov    %eax,%edi
  1276: e8 2e ff ff ff          callq  11a9 <_Z1Ai>
  127b: 5b                      pop    %rbx
  127c: c3                      retq   

R2:
  <exact same as R1>
Run Code Online (Sandbox Code Playgroud)

爪哇 ( javap -c MyClass)

javap -c Appel 
static int R1();
  Code:
     0: iconst_3
     1: invokestatic  #8                  // Method C:(I)I
     4: iconst_3
     5: invokestatic  #9                  // Method D:(I)I
     8: iadd
     9: invokestatic  #10                 // Method B:(I)I
    12: invokestatic  #11                 // Method A:(I)I
    15: ireturn

static int R2();
  Code:
     0: iconst_3
     1: invokestatic  #9                  // Method D:(I)I
     4: istore_0
     5: iconst_3
     6: invokestatic  #8                  // Method C:(I)I
     9: istore_1
    10: iload_1
    11: iload_0
    12: iadd
    13: invokestatic  #10                 // Method B:(I)I
    16: istore_2
    17: iload_2
    18: invokestatic  #11                 // Method A:(I)I
    21: ireturn
Run Code Online (Sandbox Code Playgroud)

rzw*_*oot 7

不。您所缺少的细节正是优化发生的地方。

在 C 语言中,读取源代码的应用程序(gcc例如)是执行大部分优化的应用程序(尽管越来越多的优化是由 CPU 本身在其管道和微码翻译引擎中完成的 - 并不是说​​有一个作为一名程序员,你可以做很多事情来影响这一点)。因此,该应用程序 ( gcc) 具有-o(优化级别)选项,并且该应用程序可能会消耗大量 CPU 周期来分析您的代码。

在 java 领域,这不是它的工作原理。javac已步入正轨:规范准确地规定了它应该生成的内容,几乎精确到字节——与 C 语言相反,其中规范用“mays”和“coulds”堆叠到了鳃上——编译器有很大的回旋余地,特别是包括基本“字”的位宽,而 java 将所有这些锁定在规范中,无论您最终运行的 CPU 架构的位宽如何。

优化是由java.exe- 运行时完成的。一般来说,它的方法比 C 更高效,因为与 C 不同,运行时的好处是能够检查“实时行为”,而不是 C 编译器可以做的事情(这就是为什么 C 编译器倾向于有很多“提示”系统,您可以在其中通知编译器您怀疑运行时行为可能是什么)。

所有现代 JVM 的工作方式都是低效运行代码(javac 生成的看似低效的代码,您可以通过 注意到javap -v),而且实际上运行效率甚至比这更低,因为 JVM 会添加一堆钩子来添加簿记。例如,JVM 将跟踪调用方法的频率、运行时间,以及例如,对于每个if,计算每个分支的执行频率。所有这些都使它运行得更慢。

JVM 之所以这样做,是因为对于 99%(字面意思)的所有程序来说,大约 99%(再次强调,字面意思,并不夸张)的 CPU/内存“花费”在不到 1% 的代码上,但是技巧就是,试图预测那是哪 1%。通过所有这些簿记,java 将知道,然后分析那 1% 的字节码,并使用迄今为止由于所有这些簿记而观察到的运行时行为,以得出经过微调的机器代码。这意味着 java 可以编写分支预测的代码(确保机器代码不必跳转到最常采用的分支路径),除非它不是预测:java.exe 知道哪一个是“最常采用的路径”。与gcc必须猜测的情况相比,程序员可以选择通过源文件中的分支提示来协助。

这只是数千个java.exe可以应用机器代码优化的地方之一。

这仍然意味着大约 99% 的代码运行效率非常低。但是,考虑到这仅占用不到 1% 的 CPU/内存,所以这并不重要。

由于各种因素,Java 比 C 慢,但“优化指令”不是其中之一:

  • Java 无法使用影响底层语言模型的特定于体系结构的功能,例如曾经作为协处理器的 80 位宽度变量。瓦尔哈拉计划正试图解决这个问题。
  • 更普遍的是,java 很难直接与 arch/OS 本地低级 API 接口。
  • 簿记和垃圾收集器往往有“加速时间”。它们一开始很慢,随着时间的推移会变得更快。对比。C 代码几乎以前所未有的速度运行。

当然,对于简单的命令行一次性工具(如ls或 )来说,java 既不流行,也不是一个好主意/bin/true。例如,在编写 Web 请求响应程序时,Java 的性能非常出色。它们会运行很长时间,而热点进程确实有帮助。