虚拟机操作码解析、性能测试






3.71/5 (6投票s)
2004年10月28日
4分钟阅读

59931

560
实现了四种常见操作码解析方法并进行了测试。
引言
虚拟机核心是一个解析当前操作码并执行相应操作的机制。虽然有许多方法可以完成此任务,但该主题的典型教程倾向于使用 switch-on-opcode 实现。有些人选择此方法仅仅是为了限制示例的复杂性。然而,另一些人则认为 switch-on-opcode 方法在性能方面更可取。
本文旨在
- 列举典型操作码解析方法
- 以符合使用习惯的方式实现每种方法
- 衡量并比较每种方法的性能
通过这样做,本文展示了
- switch-on-opcode 方法并不像最初看起来那么理想
- 虚函数调用的性能影响可以忽略不计(至少在此上下文中)
- 先编写,后测量和优化,是一个好策略
背景
我最近有机会为一个我正在从事的项目设计和实现了一个虚拟机。我不想开始一个受到性能问题困扰的设计,于是列出了可能的操作码解析方法列表,并对每种方法的实现进行了测试。识别出的四种可能方法是:
- 方法 0:switch-on-opcode,原地处理
- 方法 1:switch-on-opcode,处理延迟到函数
- 方法 2:函数指针数组(操作码作为数组索引)
- 方法 3:函数对象指针数组(同样,操作码作为索引)
第二种方法(方法 1)只是第一种方法的细微变体。它试图通过将每个操作码实现为自己的函数来更好地组织代码。人们期望这总是比第一种方法慢。
switch-on-opcode 方法被立即排除。不是因为性能,而是为了可扩展性(和清晰性)。对于项目来说,VM 能够轻松扩展(由应用程序程序员修改和添加操作码)非常重要。然而,为了测试目的,switch-on-opcode 方法被包含进来是为了完整性并提供比较的基准。
在测量之前,列表中方法的顺序反映了粗略的速度估计(可能从最快到最慢)。
测试代码
本文附带的代码测试了前面列出的四种方法,并将结果输出到屏幕和文件。测试分为一系列“轮次”。每一轮创建一个比前一轮更大的程序(随机操作码列表),并为每种方法多次运行该程序。为了公平起见,每“轮”测试的机器顺序是随机的,并在计算平均执行时间之前丢弃最小和最大时间。
计时测量使用 RDTSC Pentium 指令进行。由于不需要以秒为单位的时间,所有时间都以“时钟周期”为单位。在 Pentium 机器上执行是代码的唯一要求。
对于那些希望使用随附代码进行试验的人,请首先参考文件:Parameters.h。该文件定义了测试使用的一些参数。在各种参数中,VALID_OPCODE_COUNT
和 COMPLEX_OPERATION
最有趣。前者 VALID_OPCODE_COUNT
定义了虚拟机使用的指令集的大小。如果定义了 COMPLEX_OPERATION
,它将强制每个操作码使用一个非平凡的操作。否则,将使用简单的算术操作。
每种方法都实现为一个“机器”,并位于自己的文件中。在本文的其余部分,方法和机器这两个术语是同义的。
结果
测试程序执行了多次并绘制了结果。上面的两张图代表了典型结果。对于计算开销低的操作码(即简单操作),操作码解析方法对性能有明显影响。请注意,对于简单操作码,采用虚函数的方法(方法 3)比 switch-on-opcode 方法(0 和 1)明显更快。
随着操作开销的增加(即复杂操作),操作码解析技术对执行时间的影响变得不那么显著。
测试代码使用 VC++ .NET 和 GCC 3.3.3 在 2.4GHz Pentium 机器上编译并运行。600000 次计数等于 250uS。
结论
第一个结论是经常被陈述的:在设计时考虑可扩展性和可维护性问题比考虑性能问题更有建设性。在实际测量之前,无法识别和纠正真正的性能问题。
第二个结论:确保您正在测试您需要的内容。如果使用不切实际的小指令集和空的“测试”操作码来测试 VM 实现,switch-on-opcode 方法可能会因为操作码确定性的依赖性被人为地提高而表现得更好。
最后的结论:确保您正在测试您认为自己正在测试的内容。空的“测试”操作码或未使用的“测试”变量的赋值可能会被您的编译器优化掉。您可能正在进行一个您认为并非如此快速的设计。
许可证
本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。
作者可能使用的许可证列表可以在此处找到。