解释器指令入口——栈顶缓存
书接上回,转发表的结构是栈顶状态和字节码值共同组成,使用栈顶状态的原因是为了在特殊情况下提高解释器的执行速度。
例1 栈顶状态前后一致
假设由下列字节码执行序列
iload_1
iadd
iload_1字节码的含义是把本地变量表中的第1号元素整数放入栈顶,在x86上就是在rax寄存器里保存第1号元素的值。
假设现在操作数栈的布局如下

iadd指令是把栈顶的两个数相加,结果放入栈顶,那么结果就是

这种栈顶状态前后一致的操作只需要更新rax寄存器。
其实现的汇编如下
mov 本地变量表1的值, %rax
pop %rdx
add %rdx, %rax
例2 栈顶状态前后不一致
紧跟上面的指令运算结果,现在执行下面的指令
iload_2
该字节码指令是把本地变量表的第2号元素推向栈顶,即rax值不再是30,而是最新值,30会压入内存
于是,hotspot在进入该指令之前会判断当前的栈顶状态,如果栈顶是整型,则需要进行压栈操作,具体如下
push %rax
mov 本地变量表2的值, %rax
栈顶缓存
在例子2中,对已经处于栈顶的值进行了内存压栈操作,而例子1中没有已在栈顶rax中的值进行操作。这种根据栈顶状态来决定是否将rax进行压栈的操作成为栈顶缓存。即上一条指令的结果究竟是放rax还是放内存,实际是由下一条指令的操作决定的。
在hotspot中的以下代码对每个字节码的进入栈顶状态和输出栈顶状态进行了定义
/*templateTable.cpp*/
// Java spec bytecodes ubcp|disp|clvm|iswd in out generator argument
def(Bytecodes::_nop , ____|____|____|____, vtos, vtos, nop , _ );
def(Bytecodes::_aconst_null, ____|____|____|____, vtos, atos, aconst_null , _ );
def(Bytecodes::_iconst_m1 , ____|____|____|____, vtos, itos, iconst , -1 );
...
其中in和out列对输入和输出的栈顶状态进行定义。例如iconst_m1字节码的输入栈顶状态应该是void,输出栈顶状态时int
为了实现例1和例2的不同操作,hotspot在每条字节码的入口地址生成函数中对不同的栈顶状态进行了汇编生成
/*templateInterpreterGenerator_x86.cpp*/
void TemplateInterpreterGenerator::set_entry_points(Bytecodes::Code code) {
...
if (Bytecodes::is_defined(code)) {
Template* t = TemplateTable::template_for(code);
assert(t->is_valid(), "just checking");
//正常字节码指令
set_short_entry_points(t, bep, cep, sep, aep, iep, lep, fep, dep, vep);
}
if (Bytecodes::wide_is_defined(code)) {
Template* t = TemplateTable::template_for_wide(code);
assert(t->is_valid(), "just checking");
//宽字节码指令,此处不表
set_wide_entry_point(t, wep);
}
这里主要涉及代码如下
//生成字节码指令的汇编片段
Template* t = TemplateTable::template_for(code);
把不同栈顶状态的地址赋值
set_short_entry_points(t, bep, cep, sep, aep, iep, lep, fep, dep, vep);
其具体实现如下
void TemplateInterpreterGenerator::set_short_entry_points(Template* t, address& bep, address& cep, address& sep, address& aep, address& iep, address& lep, address& fep, address& dep, address& vep) {
assert(t->is_valid(), "template must exist");
switch (t->tos_in()) {
case btos:
case ztos:
case ctos:
case stos:
ShouldNotReachHere(); // btos/ctos/stos should use itos.
break;
case atos: vep = __ pc(); __ pop(atos); aep = __ pc(); generate_and_dispatch(t); break;
case itos: vep = __ pc(); __ pop(itos); iep = __ pc(); generate_and_dispatch(t); break;
case ltos: vep = __ pc(); __ pop(ltos); lep = __ pc(); generate_and_dispatch(t); break;
case ftos: vep = __ pc(); __ pop(ftos); fep = __ pc(); generate_and_dispatch(t); break;
case dtos: vep = __ pc(); __ pop(dtos); dep = __ pc(); generate_and_dispatch(t); break;
case vtos: set_vtos_entry_points(t, bep, cep, sep, aep, iep, lep, fep, dep, vep); break;
default : ShouldNotReachHere(); break;
}
}
可以看到,当栈顶为bool, byte, char 和short时,统一按照不允许操作,因为没有字节码指令进入栈顶状态为上述类型。
如果进入栈顶状态是整型,那么走itos分支
生成的代码地址分为2部分:
1.如果进入状态是vtos,即void,那么首先做pop栈顶操作,即把内存中的栈顶元素放入rax
2.如果进入状态是itos,即整型,那么直接调用字节码指令汇编generate_and_dispatch(t);
将vtos的地址赋值给vep,将itos的地址赋值给iep
栈顶缓存汇编
现在举一个极端的例子——NOP指令,该指令什么都不做,进入状态应该是void,输出状态是void,如果上一条指令的输出栈顶状态是object,则应该先将rax中的值压栈。下面是gdb调试的结果

总结
栈顶缓存的本质就是上一条指令放栈顶的位置由下一条指令决定

2319

被折叠的 条评论
为什么被折叠?



