Java字节码:局部变量表与堆栈计算

时间:2017-04-05 21:14:09

标签: java jvm jvm-bytecode

假设我们有以下课程:

import h5py

f_kwd = h5py.File("experiment1_100.raw.kwd", "r") # reads hdf5 file
dset_data = f_kwd['recordings/0/data']
print (len(dset_data)) # prints 31646700
print (dset_data[0]) # prints the following

[    94   1377    208    202    246    387   1532   1003    460    665
810    638    223    363    990     78   -139    191     63    630
763     60    682   1025    472   1113   -137    360   1216    297
-71    -35   -477   -498   -541   -557  27776   2281 -11370  32767
-28849 -30243]

list_value = []
for t_stamp in (dset_data):
    for value in t_stamp:
        if value > 400:
            list_value.append(value)

这个类广泛使用局部变量,所有这些都是最终的。

如果我们查看生成的字节码,让我们说final class Impl implements Gateway3 { private final Sensor sensor1; private final Sensor sensor2; private final Sensor sensor3; private final Alarm alarm; public Impl(Sensor sensor1, Sensor sensor2, Sensor sensor3, Alarm alarm) { this.sensor1 = sensor1; this.sensor2 = sensor2; this.sensor3 = sensor3; this.alarm = alarm; } @Override public Temperature averageTemp() { final Temperature temp1 = sensor1.temperature(); final Temperature temp2 = sensor2.temperature(); final Temperature temp3 = sensor3.temperature(); final Average tempAvg = new Average.Impl(temp1, temp2, temp3); final Temperature result = tempAvg.result(); return result; } @Override public void poll() { final Temperature avgTemp = this.averageTemp(); this.alarm.trigger(avgTemp); } 方法,我们会看到以下字节码:

averageTemp

有很多令人费解的操作码。

现在,假设使用字节码生成库,我为相同的方法生成了以下字节码:

   0: aload_0
   1: getfield      #2                  // Field sensor1:Lru/mera/avral/script/bytecode/demo/Sensor;
   4: invokeinterface #6,  1            // InterfaceMethod ru/mera/avral/script/bytecode/demo/Sensor.temperature:()Lru/mera/avral/script/bytecode/demo/Temperature;
   9: astore_1
  10: aload_0
  11: getfield      #3                  // Field sensor2:Lru/mera/avral/script/bytecode/demo/Sensor;
  14: invokeinterface #6,  1            // InterfaceMethod ru/mera/avral/script/bytecode/demo/Sensor.temperature:()Lru/mera/avral/script/bytecode/demo/Temperature;
  19: astore_2
  20: aload_0
  21: getfield      #4                  // Field sensor3:Lru/mera/avral/script/bytecode/demo/Sensor;
  24: invokeinterface #6,  1            // InterfaceMethod ru/mera/avral/script/bytecode/demo/Sensor.temperature:()Lru/mera/avral/script/bytecode/demo/Temperature;
  29: astore_3
  30: new           #7                  // class ru/mera/avral/script/bytecode/demo/Average$Impl
  33: dup
  34: aload_1
  35: aload_2
  36: aload_3
  37: invokespecial #8                  // Method ru/mera/avral/script/bytecode/demo/Average$Impl."<init>":(Lru/mera/avral/script/bytecode/demo/Temperature;Lru/mera/avral/script/bytecode/demo/Temperature;Lru/mera/avral/script/bytecode/demo/Temperature;)V
  40: astore        4
  42: aload         4
  44: invokeinterface #9,  1            // InterfaceMethod ru/mera/avral/script/bytecode/demo/Average.result:()Lru/mera/avral/script/bytecode/demo/Temperature;
  49: astore        5
  51: aload         5
  53: areturn

从语义上讲,这个新方法实现与旧方法具有相同的含义 - 它仍然需要来自三个传感器的温度值,从它们得到一个平均值并返回它。但是不是将中间值放到变量上,而是在堆栈上进行所有计算。我可以用这种方式重写它,因为我的所有局部变量和字段都是最终的。

现在有一个问题:如果我正在做一些与字节码生成相关的魔术并遵循这个&#34;堆栈上的所有计算&#34;无处不在(假设我的所有变量和字段都是最终的),我可能面临哪些潜在的陷阱?

注意:我无意以我描述的方式重写现有Java类的字节码。这里给出的示例类只是为了显示我想在字节码中实现的方法语义。

3 个答案:

答案 0 :(得分:6)

最大的陷阱:你可能会意外地阻止JIT完成它的工作。

从而实现与目标完全相反的目标:降低运行时性能。

JIT(在某种程度上)用于创建最佳结果,用于众所周知的常用编码模式。如果你的工作更加努力,很可能会做一个不那么理想的工作。

重点是:与其他语言相比,java编译器没有做很多优化步骤。真正的魔法发生在...... JIT开始的时候。因此:你必须非常详细地研究JIT正在做什么,以了解如何创建更好的字节码,以后也可以“JIT”。

答案 1 :(得分:4)

您的字节码正在消除局部变量,您也可以在Java中执行这些变量:

public Temperature averageTemp() {
    return new Average.Impl(sensor1.temperature(),
                            sensor2.temperature(),
                            sensor3.temperature()).result();
}

这将生成以下字节码:

   0: new           #38                 // class Average$Impl
   3: dup
   4: aload_0
   5: getfield      #27                 // Field sensor1:LSensor;
   8: invokevirtual #29                 // Method Sensor.temperature:()LTemperature;
  11: aload_0
  12: getfield      #34                 // Field sensor2:LSensor;
  15: invokevirtual #29                 // Method Sensor.temperature:()LTemperature;
  18: aload_0
  19: getfield      #36                 // Field sensor3:LSensor;
  22: invokevirtual #29                 // Method Sensor.temperature:()LTemperature;
  25: invokespecial #40                 // Method Average$Impl."<init>":(LTemperature;LTemperature;LTemperature;)V
  28: invokevirtual #55                 // Method Average$Impl.result:()LTemperature;
  31: areturn

那是完全你做了什么,所以这样做有问题吗?

但是,是否有理由选择其中一个?不会。无论如何,JIT编译器可能会这样做。

答案 2 :(得分:4)

Andreas’ answer所示,让Java代码利用堆栈作为临时值并不常见,例如在嵌套表达式中。这就是为什么指令集是以这种方式创建的,使用操作数堆栈来隐式地引用先前计算的值。事实上,我会过度使用局部变量来调用你的代码示例。

如果字节码生成工具的输入不是Java代码,则变量的数量可能与典型的Java代码不同,特别是如果它们具有声明性,因此不需要将所有变量直接映射到字节码中的局部变量。

像HotSpot这样的JVM将代码转移到SSA form,其中局部变量和操作数堆栈之间的所有传输操作以及dupswap等纯堆栈操作都被消除了无论如何,在应用后续优化之前,您选择是否使用局部变量不会对性能产生任何影响。

值得注意的是,您通常无法在调试器中检查操作数堆栈上的值,因此您可能会考虑在进行调试构建时保留变量(当生成LocalVariableTable时)。

某些代码构造需要局部变量。例如。当你有一个异常处理程序时,它的入口点将清除操作数堆栈,只包含对异常的引用,因此它想要访问的所有值都必须具体化为局部变量。我不知道你的输入形式是否有循环结构,如果是这样的话,你通常会在必要时将它们从声明形式转换为使用可变变量的传统循环。注意iinc instruction,它直接使用局部变量......