是否输入了一些不必要的 Java 字节码指令?
Are some Java bytecode instructions unnecessarily typed?
如 https://docs.oracle.com/javase/specs/jvms/se14/html/jvms-2.html#jvms-2.11.1 所述,将操作数类型编码为操作码是有代价的:
Given the Java Virtual Machine's one-byte opcode size, encoding types into opcodes places pressure on the design of its instruction set. If each typed instruction supported all of the Java Virtual Machine's run-time data types, there would be more instructions than could be represented in a byte.
因此,似乎应该只对需要操作数类型信息或启用优化的指令执行此操作。例如,需要区分 iadd
和 fadd
,因为整数和浮点数的加法实现方式不同。而且我不知道为什么有不同的指令从数组加载 boolean
和 int
(分别为 baload
和 iaload
),但我可以至少想象一些性能原因。
但是,为什么将int
(istore
)和float
(fstore
)存储到局部变量中的指令不同?它们不应该以完全相同的方式实现吗?
这个答案 说字节码验证器需要输入指令。但这真的有必要吗?在一个方法中,所有数据从方法的参数(其类型已知)和 class 字段(其类型也已知)流向其他 class 字段和 return 值。因此,由于输入和输出的类型是已知的,我们不能为指令重建任何缺失的类型吗?事实上,这不是字节码验证器所做的,因为它必须 检查 类型,也就是说,它必须知道哪些类型是预期的?
简而言之: 如果我们将 istore
和 fstore
组合成一条指令,会出现什么问题?性能或便携性会受到影响吗?字节码验证会停止工作吗?
我认为您关于加载和存储不需要输入的说法是正确的。我自己不是 Java 的原始设计者之一,我只能推测为什么会这样设计。但这是我的猜测。
我认为当 Java 最初设计时,它的设计考虑到了解释,并且他们可能认为加载和存储需要针对 int 和 float 进行不同的实现。也有可能他们希望能够 运行 无需验证(事实上,今天仍然可以禁用字节码验证)。最后,虽然在加载和存储指令未类型化时仍然可以执行相同的字节码验证在技术上是可行的,但这会使事情稍微复杂一些。
验证可以用非类型化加载和存储完成的一个简单证明是局部变量 table 的行为类似于操作数堆栈,并且有非类型化指令在操作数堆栈上运行(dup
、swap
、等等)。
istore
和 fstore
在几乎每个 JVM 和我曾经使用过的每个架构上的实现方式都不同。
例如,在 HotSpot JVM x64 解释器中,istore_0
被实现为
mov dword ptr [r14], eax
而 fstore_0
实现为
movss dword ptr [r14], xmm0
解释器将栈顶值缓存在寄存器中,整数和浮点值有不同的寄存器。
类似地,baload
和 iaload
的实现方式不同,因为它们使用不同的偏移乘数(分别为 1 和 4),并且需要不同的机器指令来加载 8 位值与 32-位值。
正如您所注意到的,一些类型信息可以从数据流分析中得出,字节码验证器无论如何都会这样做。但是为了在运行时使用这些信息,栈槽和局部变量需要以某种方式标记为相应的类型,合并后的字节码指令需要在运行时读取这个标记并根据标记进行调度。当然,这在运行时性能和已用内存方面都不是最佳选择。
如 https://docs.oracle.com/javase/specs/jvms/se14/html/jvms-2.html#jvms-2.11.1 所述,将操作数类型编码为操作码是有代价的:
Given the Java Virtual Machine's one-byte opcode size, encoding types into opcodes places pressure on the design of its instruction set. If each typed instruction supported all of the Java Virtual Machine's run-time data types, there would be more instructions than could be represented in a byte.
因此,似乎应该只对需要操作数类型信息或启用优化的指令执行此操作。例如,需要区分 iadd
和 fadd
,因为整数和浮点数的加法实现方式不同。而且我不知道为什么有不同的指令从数组加载 boolean
和 int
(分别为 baload
和 iaload
),但我可以至少想象一些性能原因。
但是,为什么将int
(istore
)和float
(fstore
)存储到局部变量中的指令不同?它们不应该以完全相同的方式实现吗?
这个答案 说字节码验证器需要输入指令。但这真的有必要吗?在一个方法中,所有数据从方法的参数(其类型已知)和 class 字段(其类型也已知)流向其他 class 字段和 return 值。因此,由于输入和输出的类型是已知的,我们不能为指令重建任何缺失的类型吗?事实上,这不是字节码验证器所做的,因为它必须 检查 类型,也就是说,它必须知道哪些类型是预期的?
简而言之: 如果我们将 istore
和 fstore
组合成一条指令,会出现什么问题?性能或便携性会受到影响吗?字节码验证会停止工作吗?
我认为您关于加载和存储不需要输入的说法是正确的。我自己不是 Java 的原始设计者之一,我只能推测为什么会这样设计。但这是我的猜测。
我认为当 Java 最初设计时,它的设计考虑到了解释,并且他们可能认为加载和存储需要针对 int 和 float 进行不同的实现。也有可能他们希望能够 运行 无需验证(事实上,今天仍然可以禁用字节码验证)。最后,虽然在加载和存储指令未类型化时仍然可以执行相同的字节码验证在技术上是可行的,但这会使事情稍微复杂一些。
验证可以用非类型化加载和存储完成的一个简单证明是局部变量 table 的行为类似于操作数堆栈,并且有非类型化指令在操作数堆栈上运行(dup
、swap
、等等)。
istore
和 fstore
在几乎每个 JVM 和我曾经使用过的每个架构上的实现方式都不同。
例如,在 HotSpot JVM x64 解释器中,istore_0
被实现为
mov dword ptr [r14], eax
而 fstore_0
实现为
movss dword ptr [r14], xmm0
解释器将栈顶值缓存在寄存器中,整数和浮点值有不同的寄存器。
类似地,baload
和 iaload
的实现方式不同,因为它们使用不同的偏移乘数(分别为 1 和 4),并且需要不同的机器指令来加载 8 位值与 32-位值。
正如您所注意到的,一些类型信息可以从数据流分析中得出,字节码验证器无论如何都会这样做。但是为了在运行时使用这些信息,栈槽和局部变量需要以某种方式标记为相应的类型,合并后的字节码指令需要在运行时读取这个标记并根据标记进行调度。当然,这在运行时性能和已用内存方面都不是最佳选择。