3

假设我们有以下类:

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

有很多存储操作码。

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

   0: new           #18                 // class ru/mera/avral/script/bytecode/demo/Average$Impl
   3: dup
   4: aload_0
   5: getfield      #20                 // Field sensor1:Lru/mera/avral/script/bytecode/demo/Sensor;
   8: invokeinterface #25,  1           // InterfaceMethod ru/mera/avral/script/bytecode/demo/Sensor.temperature:()Lru/mera/avral/script/bytecode/demo/Temperature;
  13: aload_0
  14: getfield      #27                 // Field sensor2:Lru/mera/avral/script/bytecode/demo/Sensor;
  17: invokeinterface #25,  1           // InterfaceMethod ru/mera/avral/script/bytecode/demo/Sensor.temperature:()Lru/mera/avral/script/bytecode/demo/Temperature;
  22: aload_0
  23: getfield      #29                 // Field sensor3:Lru/mera/avral/script/bytecode/demo/Sensor;
  26: invokeinterface #25,  1           // InterfaceMethod ru/mera/avral/script/bytecode/demo/Sensor.temperature:()Lru/mera/avral/script/bytecode/demo/Temperature;
  31: invokespecial #33                 // 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
  34: invokevirtual #36                 // Method ru/mera/avral/script/bytecode/demo/Average$Impl.result:()Lru/mera/avral/script/bytecode/demo/Temperature;
  37: areturn

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

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

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

4

3 回答 3

7

最大的陷阱:您可能会不小心阻止 JIT 完成其工作。

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

JIT(在一定程度上)是为了为众所周知的、经常使用的编码模式创建最佳结果而编写的。如果你让它的工作变得更难,它很可能会做得不太理想。

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

于 2017-04-05T21:21:48.807 回答
4

正如Andreas 的回答所示,让 Java 代码利用堆栈获取临时值并不罕见,例如在嵌套表达式中。这就是为什么以这种方式创建指令集的原因,它使用操作数堆栈来隐式引用先前计算的值。事实上,我将您的代码示例称为过度使用局部变量的异常。

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

像 HotSpot 这样的 JVM 将代码转换为SSA 形式,其中在应用后续优化之前,局部变量和操作数堆栈之间的所有传输操作,以及像dup和这样的纯堆栈操作都会swap被消除,因此您可以选择是否使用局部变量不会对性能产生任何影响。

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

一些代码结构需要局部变量。例如,当您有一个异常处理程序时,它的入口点将清除操作数堆栈,只包含对异常的引用,因此它想要访问的所有值都必须被具体化为局部变量。我不知道您的输入表单是否具有循环结构,如果是这样,您通常会在必要时使用引擎盖下的可变变量将它们从声明形式转换为常规循环。注意iinc指令,它直接与局部变量一起工作......</p>

于 2017-04-06T11:31:35.297 回答
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 编译器无论如何都会这样做。

于 2017-04-05T21:28:30.850 回答