前言
语雀地址:https://www.yuque.com/yangxiaofei-vquku/wmp1zm/mggrlr
基于栈式的架构的虚拟机所使用的零地址指令更加紧凑,单完成一项操作的时候必然需要使用更多的入栈和出栈指令,虚拟机栈也是存在于内存中,这就意味着将需要更多的指令分派次数和内存读/写次数,频繁的执行内存读/写操作必然会影响执行速度,为了解决这一问题HotSpot JVM的设计者们提出了栈顶缓存(Top-of-Stack-Caching)技术,将栈顶元素(或栈顶周边)元素缓存到物理CUP的寄存器中,以此降低对内存的读写次数,提升执行引擎的执行效率。
有关栈顶缓存技术需要关注两个核心问题:
- 缓存了栈顶附近的多少个元素?如果缓存了n个元素,那么就叫n-TOS caching;
- 缓存带有多少种“状态”?如果有n种状态那么就叫n-state TOS caching。
1. 先看n-TOS caching
从抽象数据结构来举例操作数栈的实现:可以想像把Java标准库自带的那个java.util.Stack包装一下,假如实现栈顶缓存技术的逻辑如下:压如操作数栈的栈顶元素优先存到CUP的寄存器中。(假设Stack为原始操作数栈,StackWith1TOSCA为支持栈顶缓存的操作数栈)
import java.util.EmptyStackException;
import java.util.Stack;
public class StackWith1TOSCA<E> {
private enum TosState {
NOT_CACHED,
CACHED;
}
// 内存中的操作数栈(内存)
private Stack<E> theStack = new Stack<E>();
// 栈顶元素(寄存器)
private E topOfStackElement; // the cache
// 是否有缓存,在1-TOS中即表示栈顶是否有数据
private TosState state = TosState.NOT_CACHED;
// 向操作数栈中压如元素
public void push(E elem) {
// 如果栈顶已有元素,
if (state == TosState.CACHED) {
// 将栈顶元素压入内存中真正的操作数栈
theStack.push(topOfStackElement);
}
// 将寄存器中栈顶数据替换为新压人的栈顶元素
topOfStackElement = elem;
// 设置state为CACHE代表栈顶有元素
state = TosState.CACHED;
}
// 操作数栈的栈顶元素出栈
public E pop() {
// 如果状态为NOT_CACHE也就是无栈顶元素,此时操作数栈为空无法出栈
if (state == TosState.NOT_CACHED) throw new EmptyStackException();
// 将CPU寄存器中的栈顶元素取出
E result = topOfStackElement;
// 内存中的栈桢操作数栈是否为空
if (theStack.isEmpty()) {
// 此时操作数栈为空,栈顶元素弹出为空
state = TosState.NOT_CACHED;
topOfStackElement = null;
} else {
// 将内存中操作数栈的栈顶元素去除放入寄存器
topOfStackElement = theStack.pop();
}
return result;
}
}
那么如果有这样的Java代码:
static void foo(Object o) {
Object temp = o;
}
对于字节码指令为
用不支持栈顶缓存的操作数栈Stack执行上面字节码指令,执行了2次内存读取两次内存写入,jvm操作为
// 从局部变量表读取写入操作数栈——》一次内存读取,一次内存写入
stack.push(locals[0]);
// 从操作数栈中读取写入局部变量表——》一次内存读取,一次内存写入
locals[1] = stack.pop();
用支持栈顶缓存技术的操作数栈StackWith1TOSCA执行上面字节码指令,执行了一次内存读取,一次寄存器写入,一次寄存器读取,一次内存写入,jvm操作为
// 从局部变量表读取,写入寄存器——》一次内存读取,一次寄存器写入
topOfStackElement = locals[0];
// 从寄存器读取,写入局部变量表——》一次寄存器读取,一次内存写入
locals[1] = topOfStackElement;
从上述例子可以看出将栈顶元素缓存寄存器有效的减少了内存的读写次数,而如果选择把栈顶附近的若干个元素缓存在寄存器里的话,在频繁的计算逻辑中将大幅度提升性能。
2.再看n-state TOS caching
前面的StackWith1TOSCA例子里可以看到已经有“state”的概念出现:我们必须要知道当前在缓存里到底有没有值,不然就无从判断压栈/出栈时数据该何去何从了。这个例子用了两种状态,NOT_CACHED和CACHED;对于不关心栈里元素类型的stack caching来说,1-TOS用两种状态就够用了。
实际上“状态”可以记录许多东西,取决于到底要实现怎样的TOSCA。
- 一个例子:如果我们现在不用1-TOS,而用3-TOS caching的话,那很明显我们的“状态”不但要记录“有没有缓存栈顶元素”,还得记录“到底栈顶附近的三个元素到底放在哪个变量里了”。
- 另一个例子:如果我们的栈需要跟踪栈里的元素的类型,同时我们使用1-TOS caching的话,那就意味着要记录的“状态”里必须记住栈顶元素是什么类型的。HotSpot VM的解释器就是这样的例子,它虽然只用了1-TOS caching,但它的TosState却有9种有效值。也就是说这个解释器的TOSCA可以描述为1-TOS, 9-state caching。
大家可以想像一个n > 1的n-TOS如果跟带类型的TOSCA结合起来状态数量的膨胀速度会有多快。
实际上多数虚拟机就算用了stack caching也只会用1-TOS,因为简单高效;大不了1-TOS外带类型。
也有复杂一些的例子,例如Sun JDK 1.1.x里的解释器在x86上的实现,它用的是2-TOS, 3-state caching。