机器指令翻译成 JavaScript —— No.6 深度优化

时间:2022-01-01 19:33:59

第一篇 中我们曾提到,JavaScript 最终还得经过浏览器来解析。因此可以把一些优化工作,交给脚本引擎来完成。

现代浏览器的优化能力确实很强,但是,运行时的优化终归是有限的。如果能在事先实现,则可投入更多资源,优化得更充分。

优化尝试

指令 1:1 翻译成 JS,结果显然会存在一些累赘的逻辑。

例如状态标志,很多时候是不必计算的:

A = read(10)	// LDA 10
SR_N = ... // 无意义,可以去除
SR_Z = ... X = read(20) // LDX 20
SR_N = ... // 值得商榷
SR_Z = ... if (!SR_Z) { // BNE ..
nextFn = ...
return
} Y = read(30) // LDY 30
SR_N = ...
SR_Z = ...

第一步的状态标志,会被后面的覆盖,显然是没必要计算的。但第二步,就有一些奥妙了。

如果 !SR_Z 成立,那么进入下一个流程块;否则执行 LDY,状态被重置。因此,要不要计算 SR_N,可以先看 SR_Z 的结果。

于是,可将顺序调整成:

X = read(20)	// LDX 20
SR_Z = ... if (!SR_Z) { // BNE ..
SR_N = ... // 移到这里
...
}

这样,就能避免无意义的计算。

此外,再对表达式进行合并,最终变成:

A = read(10)	// LDA 10
X = read(20) // LDX 20 if (X != 0) { // BNE ..
SR_N = (X > 127)
nextFn = ...
return
} Y = read(30) // LDY 30
SR_N = (Y > 127)
SR_Z = (Y == 0)

看起来就更精简了。

再进一步,若能发现 nextFn 不依赖该流程的状态标记,那么 SR 的计算也可以省略!


其他,还有诸如 inline 优化。例如一些简单的跳转:

	JSR L1
XXX 1
XXX 2
L1:
XXX 3
XXX 4
RTS

直接将其展开,于是变成:

	XXX 3
XXX 4
XXX 1
XXX 2

这样就能减少指令块的分割,降低流程模拟的开销。

类似的优化方案还有很多。但是,若想亲自实现这些,则需要深厚的理论知识。并且实现起来也很麻烦,完全不适合这种周末时间的小研究。

借用工具

理论知识固然重要,但工程师的宗旨,就是最大程度利用现有资源,将想法快速变成现实。

这几篇文章都只是理论探讨,实现未必就得如此。事实上,我们应该借助现成的工具,快速验证想法。

既然前面提到优化,那就选择一个强大的优化方案 —— LLVM。

我们可以将 6502 指令逻辑,转换成 LLVM 中间表示。这样,即可用现成工具进行优化。

但是这么做有些麻烦。一是对 LLVM 不太熟悉,二是调试起来也不方便。

对于 LLVM 来说,最适合的输入语言当然是 C 了,毕竟 clang 就是为此产生的。

机器指令翻译成 JavaScript —— No.6 深度优化

所以,我们不妨将 6502 指令翻译成 C 语言。C 不仅简单优雅,而且借助 IDE 开发调试都超级方便。

下一篇,我们尝试这个方案是否可行。