Java 字节码:局部变量 table vs 栈上计算
Java bytecode: local variables table vs on-stack calculation
假设我们有以下 class:
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);
}
这个class广泛使用了局部变量,而且都是final。
如果我们查看为 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
store 操作码很多。
现在,假设使用字节码生成库,我用相同的方法生成了以下字节码:
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
在语义上,与旧方法相比,这种新方法实现具有相同的含义 - 它仍然从三个传感器获取温度值,从中取平均值并 returns 它。但是它不是将中间值赋给变量,而是在堆栈上进行所有计算。我可以那样重写它,因为我所有的局部变量和字段都是最终的。
现在有一个问题:如果我正在做一些与字节码生成相关的魔术并在任何地方都遵循这种 "all calculations on stack" 方法(假设我所有的变量和字段都是最终的),我可能会面临哪些潜在的陷阱?
注意:我无意按照我描述的方式为现有 Java classes 重写字节码。这里给出示例class只是为了展示我想在我的字节码中实现的方法语义。
最大的陷阱:您可能会不小心阻止 JIT 完成它的工作。
从而实现与您的目标完全相反的结果:降低运行时性能。
JIT(在某种程度上)是为了为众所周知的、经常使用的编码模式创造最佳结果而编写的。如果你让它的工作变得更难,它很可能会做得不太好。
重点是:与其他语言相比,java 编译器没有做很多优化步骤。真正的魔法发生在以后……当 JIT 启动时。因此:您必须非常详细地研究 JIT 正在做什么,以了解如何创建更好的字节码,以后也可以 "JITed" 很好。
您的字节码正在消除局部变量,您也可以在 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 编译器可能无论如何都会这样做。
如 所示,Java 代码将堆栈用于临时值并不罕见,例如在嵌套表达式中。这就是指令集以这种方式创建的原因,使用操作数堆栈来隐式引用先前计算的值。事实上,我认为你的代码示例过度使用局部变量是不寻常的。
如果您的字节码生成工具的输入不是 Java 代码,变量的数量可能与典型的 Java 代码不同,特别是如果它们是声明性的,所以有不需要将它们全部直接映射到字节码中的局部变量。
像 HotSpot 这样的 JVM 将代码转移到一个 SSA form,其中所有局部变量和操作数堆栈之间的转移操作,以及像 dup
和 swap
这样的纯堆栈操作,在应用后续优化之前,无论如何都会被消除,因此您选择使用或不使用局部变量不会对性能产生任何影响。
可能值得注意的是,您通常无法在调试器中检查操作数堆栈上的值,因此您可能会考虑在进行调试构建时保留变量(当 LocalVariableTable
也已生成时)。
一些代码结构需要局部变量。例如。当你有一个异常处理程序时,它的入口点将清除操作数堆栈,只包含对异常的引用,因此它想要访问的所有值都必须具体化为局部变量。我不知道你的输入形式是否有循环结构,如果有,你通常会在必要时使用可变变量将它们从声明形式转换为常规循环。注意 iinc
instruction,它直接与局部变量一起使用……
假设我们有以下 class:
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);
}
这个class广泛使用了局部变量,而且都是final。
如果我们查看为 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
store 操作码很多。
现在,假设使用字节码生成库,我用相同的方法生成了以下字节码:
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
在语义上,与旧方法相比,这种新方法实现具有相同的含义 - 它仍然从三个传感器获取温度值,从中取平均值并 returns 它。但是它不是将中间值赋给变量,而是在堆栈上进行所有计算。我可以那样重写它,因为我所有的局部变量和字段都是最终的。
现在有一个问题:如果我正在做一些与字节码生成相关的魔术并在任何地方都遵循这种 "all calculations on stack" 方法(假设我所有的变量和字段都是最终的),我可能会面临哪些潜在的陷阱?
注意:我无意按照我描述的方式为现有 Java classes 重写字节码。这里给出示例class只是为了展示我想在我的字节码中实现的方法语义。
最大的陷阱:您可能会不小心阻止 JIT 完成它的工作。
从而实现与您的目标完全相反的结果:降低运行时性能。
JIT(在某种程度上)是为了为众所周知的、经常使用的编码模式创造最佳结果而编写的。如果你让它的工作变得更难,它很可能会做得不太好。
重点是:与其他语言相比,java 编译器没有做很多优化步骤。真正的魔法发生在以后……当 JIT 启动时。因此:您必须非常详细地研究 JIT 正在做什么,以了解如何创建更好的字节码,以后也可以 "JITed" 很好。
您的字节码正在消除局部变量,您也可以在 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 编译器可能无论如何都会这样做。
如
如果您的字节码生成工具的输入不是 Java 代码,变量的数量可能与典型的 Java 代码不同,特别是如果它们是声明性的,所以有不需要将它们全部直接映射到字节码中的局部变量。
像 HotSpot 这样的 JVM 将代码转移到一个 SSA form,其中所有局部变量和操作数堆栈之间的转移操作,以及像 dup
和 swap
这样的纯堆栈操作,在应用后续优化之前,无论如何都会被消除,因此您选择使用或不使用局部变量不会对性能产生任何影响。
可能值得注意的是,您通常无法在调试器中检查操作数堆栈上的值,因此您可能会考虑在进行调试构建时保留变量(当 LocalVariableTable
也已生成时)。
一些代码结构需要局部变量。例如。当你有一个异常处理程序时,它的入口点将清除操作数堆栈,只包含对异常的引用,因此它想要访问的所有值都必须具体化为局部变量。我不知道你的输入形式是否有循环结构,如果有,你通常会在必要时使用可变变量将它们从声明形式转换为常规循环。注意 iinc
instruction,它直接与局部变量一起使用……