最佳的虚拟机/字节码解释器循环

6
我的项目有一个VM,可以执行从特定领域语言编译的字节码。我正在寻找提高字节码执行时间的方法。首先,我想看看是否有一种方法可以在尝试机器代码编译之前简单地改进字节码解释器。
解释器的主循环如下:
while(true)
{
  uint8_t cmd = *code++;
  switch( cmd )
  {
    case op_1: ...; break;
    ...
  }
}

问题:有没有更快的方法来实现这个循环,而不需要使用汇编语言?

我所看到的一个选择是使用GCC特定的动态goto和标签地址。与在每个case结尾处的break不同,我可以直接跳转到下一条指令。我希望优化器能够为我完成这个任务,但是查看反汇编代码后发现它并没有这样做:大多数op_codes的末尾都有一个重复的常量跳转。

如果相关,VM是一个简单的基于寄存器的机器,具有浮点和整数寄存器(每种8个)。没有堆栈,只有全局堆(语言不是那么复杂)。

3个回答

3

一个非常简单的优化是,不要使用switch/case/case/case/case/case的方式,

而是定义一个函数指针数组(每个函数都会处理特定的命令,或者一些命令,在这种情况下,您可以将几个条目设置为相同的函数,函数本身可以检查确切的代码),然后使用该数组代替switch/case语句。

switch(cmd)

只需执行a操作

array[cmd]()

这是在假设您没有太多命令的情况下给出的建议。如果您没有定义所有可能的命令(也许您只有300个命令,但您必须使用2个字节来表示它们,所以不要定义一个具有65536个项的数组,只需检查命令是否小于301,如果不是,则不进行查找)。
如果您不这样做,至少请将最常用的命令排序,使其位于switch语句的开头。
否则,可以考虑使用哈希表,但我假设您没有那么多命令,在这种情况下,执行哈希函数的开销可能比不使用switch更高。(或者使用一个非常简单的哈希函数)。

1
使用函数调用而不是 switch 语句来实现代码,速度上的提升可能性极小(函数调用会带来显著的开销)。无论如何,switch 语句应该被编译成跳转表(至少 GCC 是这样做的)。 - edA-qa mort-ora-y
很酷,我不知道gcc有这个功能,但是无论如何,我们可以将函数定义为内联函数,而不是传递任何参数,使它们成为全局变量等等,从而使函数调用开销非常小。当然,当涉及到这个级别时,最好还是直接使用汇编语言编写整个程序(如果你知道如何做的话)。 - Cray
1
你不能内联调用通过调度表进行的调用。最接近的选择是使用GCC标签地址,这基本上允许您在跳转表中创建自己的内联函数。 - edA-qa mort-ora-y
当仅通过函数地址(即通过函数指针)调用函数时,该调用极不可能被内联。 - ildjarn
好的,关于内联函数,请忽略我的话。 - Cray

1

这个架构是什么?使用字对齐的操作码可能会加速,但会增加代码大小,这意味着您需要权衡缓存未命中的成本。


x86_64。这肯定会使我的代码大小膨胀,但代码非常小。 - edA-qa mort-ora-y
转念一想,这可能行不通,因为代码与内联数据(常量)交错在一起。为了保持对齐,每个东西都必须增长到8字节。那将是相当大的代价(我还是会尝试一下)。 - edA-qa mort-ora-y
哎呀,真遗憾。回到起点重新开始吧。 - regularfry

-1

我看到一些明显的优化包括:

  1. 如果你在任何地方都没有使用cmd,那么直接使用指针间接寻址switch( *code++ )。对于长时间运行的while(true)循环,这可能有所帮助。
  2. switch()中,你可以使用continue而不是break。因为当在if/elseswitch内部使用continue时,编译器知道执行必须跳转到外部循环;但是对于break(相对于switch),情况并非如此。 希望这能帮到你。

点1:我之前尝试过,但没有任何区别(优化器可能认为它是相同的)。 点2:实际上我确实这样做了,似乎有一点点不同(奇怪的是优化器自己不会这样做,但时间差异非常小,很难排除统计异常)。 - edA-qa mort-ora-y

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接