C++ VM解释器-加权较大指令集/调度循环的性能优缺点 我正在开发一个简单的虚拟机,我在一个十字路口的中间。< /P>

C++ VM解释器-加权较大指令集/调度循环的性能优缺点 我正在开发一个简单的虚拟机,我在一个十字路口的中间。< /P>,c++,performance,instruction-set,vm-implementation,C++,Performance,Instruction Set,Vm Implementation,我最初的目标是使用字节长的指令,因此是一个小循环和一个快速计算的goto调度 然而,事实证明,事实离它不远了——256远远不足以涵盖有符号和无符号8位、16位、32位和64位整数、浮点和双精度、指针操作以及不同的寻址组合。一种选择是不实现byte和shorts,但目标是使VM支持完整的C子集以及向量操作,因为它们几乎无处不在,尽管在不同的实现中 所以我切换到了16位指令,所以现在我还可以添加可移植的SIMD内部函数和更多编译的公共例程,它们通过不被解释来真正节省性能。还有全局地址的缓存,最初编译

我最初的目标是使用字节长的指令,因此是一个小循环和一个快速计算的goto调度

然而,事实证明,事实离它不远了——256远远不足以涵盖有符号和无符号8位、16位、32位和64位整数、浮点和双精度、指针操作以及不同的寻址组合。一种选择是不实现byte和shorts,但目标是使VM支持完整的C子集以及向量操作,因为它们几乎无处不在,尽管在不同的实现中

所以我切换到了16位指令,所以现在我还可以添加可移植的SIMD内部函数和更多编译的公共例程,它们通过不被解释来真正节省性能。还有全局地址的缓存,最初编译为基指针偏移量,第一次编译地址时,它只是覆盖偏移量和指令,以便下次直接跳转时,以指令每次使用全局地址为代价,在集合中增加额外的指令

由于我不在分析阶段,我陷入了两难境地,额外的指令是否值得更大的灵活性,是否存在更多的指令,因此没有来回复制指令来弥补增加的分派循环大小?请记住,每个说明都只是几个装配说明,例如:

    .globl  __Z20assign_i8u_reg8_imm8v
    .def    __Z20assign_i8u_reg8_imm8v; .scl    2;  .type   32; .endef
__Z20assign_i8u_reg8_imm8v:
LFB13:
    .cfi_startproc
    movl    _ip, %eax
    movb    3(%eax), %cl
    movzbl  2(%eax), %eax
    movl    _sp, %edx
    movb    %cl, (%edx,%eax)
    addl    $4, _ip
    ret
    .cfi_endproc
LFE13:
    .p2align 2,,3
    .globl  __Z18assign_i8u_reg_regv
    .def    __Z18assign_i8u_reg_regv;   .scl    2;  .type   32; .endef
__Z18assign_i8u_reg_regv:
LFB14:
    .cfi_startproc
    movl    _ip, %edx
    movl    _sp, %eax
    movzbl  3(%edx), %ecx
    movb    (%ecx,%eax), %cl
    movzbl  2(%edx), %edx
    movb    %cl, (%eax,%edx)
    addl    $4, _ip
    ret
    .cfi_endproc
LFE14:
    .p2align 2,,3
    .globl  __Z24assign_i8u_reg_globCachev
    .def    __Z24assign_i8u_reg_globCachev; .scl    2;  .type   32; .endef
__Z24assign_i8u_reg_globCachev:
LFB15:
    .cfi_startproc
    movl    _ip, %eax
    movl    _sp, %edx
    movl    4(%eax), %ecx
    addl    %edx, %ecx
    movl    %ecx, 4(%eax)
    movb    (%ecx), %cl
    movzwl  2(%eax), %eax
    movb    %cl, (%eax,%edx)
    addl    $8, _ip
    ret
    .cfi_endproc
LFE15:
    .p2align 2,,3
    .globl  __Z19assign_i8u_reg_globv
    .def    __Z19assign_i8u_reg_globv;  .scl    2;  .type   32; .endef
__Z19assign_i8u_reg_globv:
LFB16:
    .cfi_startproc
    movl    _ip, %eax
    movl    4(%eax), %edx
    movb    (%edx), %cl
    movzwl  2(%eax), %eax
    movl    _sp, %edx
    movb    %cl, (%edx,%eax)
    addl    $8, _ip
    ret
    .cfi_endproc
此示例包含以下说明:

  • 将立即数值中的无符号字节分配给寄存器
  • 将无符号字节从寄存器分配到寄存器
  • 将全局偏移量中的无符号字节分配给寄存器和缓存,并更改为直接指令
  • 将全局偏移量中的无符号字节分配给寄存器(现在缓存的早期版本)
  • 。。。等等
当然,当我为它生成一个编译器时,我将能够测试生产代码中的指令流,并优化内存中指令的排列,以便将常用的指令打包在一起并获得更多的缓存命中

我只是很难想象这样的策略是否是一个好主意,膨胀将弥补灵活性,但性能呢?更多的编译例程会弥补更大的分派循环吗?是否值得缓存全局地址

我还希望有人,在汇编方面表现得体,对GCC生成的代码的质量发表意见——是否存在明显的低效和优化空间?为了说明情况,有一个
sp
指针,指向实现寄存器的堆栈(没有其他堆栈),
ip
逻辑上是当前指令指针,
gp
是全局指针(未引用,作为偏移量访问)

编辑:此外,这是我执行以下说明的基本格式:

INSTRUCTION assign_i8u_reg16_glob() { // assign unsigned byte to reg from global offset
    FETCH(globallAddressCache);
    REG(quint8, i.d16_1) = GLOB(quint8);
    INC(globallAddressCache);
}
FETCH返回对结构的引用,指令基于操作码使用该结构

REG从偏移量返回对寄存器值T的引用

全局从缓存的全局偏移量(实际上是绝对地址)返回对全局值的引用

INC只是将指令指针增加指令的大小

有些人可能会反对使用宏,但使用模板时可读性要差得多。这样代码就很明显了

编辑:我想对这个问题补充几点:

  • 我可以选择一种“仅寄存器操作”的解决方案,它只能在寄存器和“内存”之间移动数据,可以是全局的,也可以是堆。在这种情况下,每个“全局”和堆访问都必须复制、修改或使用该值,并将其移回更新。这样我就有了一个更短的分派循环,但是对于每个处理非寄存器数据的指令,都有一些额外的指令。因此,进退两难的是,本机代码的直接跳转时间要长几倍,解释指令的数量要多几倍,分派循环也要短几倍。一个短的分派循环是否能给我足够的性能来弥补额外和昂贵的内存操作?也许短调度循环和长调度循环之间的增量不足以产生真正的差异?在缓存命中率方面,在组装跳跃的成本方面

  • 我可以使用额外的解码,并且只使用8位宽的指令,但是,这可能会在处理该指令的任何位置添加另一个跳转,然后将时间浪费在跳转到处理特定寻址方案的情况上,或者浪费在解码操作和更复杂的执行方法上。在第一种情况下,调度循环仍然在增长,再加上一次跳跃。第二个选项-寄存器操作可用于解码寻址,但为了寻址任何内容,将需要具有更多未知编译时间的更复杂指令。我真的不确定这将如何与更短的调度循环叠加,再一次,不确定我的“更短和更长的调度循环”与汇编指令的长短、它们所需的内存和执行速度之间的关系

  • 我可以选择“多指令”解决方案——调度循环要大几倍,但它仍然使用预先计算的直接跳转。复杂寻址针对每一条指令都是特定的和优化的,并编译为本机的,因此“仅寄存器”方法所需的额外内存操作将被编译并主要在寄存器上执行,这对性能有好处。一般来说,这个想法是增加更多的指令集,但也增加了可以提前编译并在单个“指令”中完成的工作量。loner指令集还意味着更长的调度循环、更长的跳转(尽管可以优化以最小化)、更少的延迟
    do
    {
      pc = pc->func(pc, &context);
    } while(pc);
    
    INSTRUCTION *add_instruction(INSTRUCTION *pc, EXECUTION_CONTEXT *context)
    {
      context->op_stack[0] += pc->operand;
      return pc+1;
    }
    
    INSTRUCTION *add_instruction(INSTRUCTION *pc, EXECUTION_CONTEXT *context)
    {
      CONTEXT_ITEM *op_stack = context->op_stack;
      op_stack[0].asInt64 += op_stack[pc->operand].asInt64;
      return pc+1;
    }