测试
我有一个复古游戏项目的构想,该项目需要一个在浏览器中流畅运行并且方便定制的 MOS 6502 模拟器。由于我只需要仿真最基本的功能(不需要中断、计时精度,以及周期),所以我认为能很快完成。本文不是说明如何实现这个实际复古游戏项目的,而是我使用各种Web函数式语言生成代码的性能体验。
正如我通常所做的那样,我从 Haskell 实现执行器规范作为开始,以确保我对6502各种指令的细节理解是正确的。这个 Haskell 实现没什么特别的:外部世界被建模为 MonadIO m => MonadMachine m 类,而 CPU 本身在MonadMachine m => ReaderT CPU m中运行,在寄存器的CPU记录中 使用IORefs 。
语言
消除所有的扰动花了一整天,但一旦它满足要求,就该进行下一步了:用一种可以针对浏览器的语言重写它。 一个直观的选择是purescript :实际项目中广泛使用,因此它足够成熟,而且考虑到我的 Haskell 代码如此简单,与 Haskel 相比,PureScript 的特质应该不会真正发挥超出语法级别的作用。让我一直烦恼的一件事是数字常量没有重载,所以我代码中的所有 Word8都必须手动进行 fromIntegral; 而且,在8位 CPU 的仿真器中,有大量的Word8常量…
第二个选择是Idris 2。我在编写ICFP Bingo网络应用程序时,对 Idris 1 有很好的体验,但那个项目完全是关于 DOM 操作而不是计算。我很好奇我可以从 Idris 2 的 JavaScript 后端获得什么性能。
然后我不得不考虑Asterius,一个基于 GHC 的编译器,发出 WebAssembly。它的 GitHub 页面说明它“由 Tweag I/O 积极维护”,但它实际上处于相当糟糕的状态:相关构建文档已经过时,所以尝试它的唯一方法是使用 20G Docker 容器…
讨论的列表中缺少了 GHCJS。不幸的是,我找不到它的最新版本;看起来这个项目,或者至少是与 Stack 等标准 Haskell 工具集成的工作,已经停止了。
为了比较性能,我将相同的内存映像加载到各种仿真器中,将程序计数器设置为相同的起点,并运行 4142 条指令,直到达到某个目标指令。为了掩盖浏览器的 JavaScript JIT 引擎等,每个测试首先运行 100 次作为预热,然后进行 100 次测试。
除了 PureScript、Idris 2 和 GHC/Asterius 实现之外,我还添加了第四个版本作为基线:vanilla JavaScript。当然,我尽量让它接近功能版本;我希望我写的内容接近编译器输出的合理预期。
性能结果
以下数据来自 该 GitHub 存储库中收集的实现。PureScript 和 Idris 2 版本已根据各自 Discord 频道的想法进行了改进。对于 PureScript,使用 Reader的 CPS 转换版本很有帮助;在 Idris 2 的情况下,Stefan Höck更改参数而不是ReaderT,并在循环指令时使用 PrimIO,显着提高了性能。
可见,Idris 2 在这里遥遥领先:除非您愿意使用 底层JavaScript 进行编程,否则它是目前为止最佳的选择,无论是对于微小的部署规模还是出色的性能。剩下需要改进的是更好地编译 monad 转换器堆栈,以便原始ReaderT代码与使用隐式参数的版本一样工作
要自己运行基准测试,请检查GitHub 存储库,在顶级目录中 运行make ,然后使用 Web 浏览器打开_build/index.html并使用 JavaScript 控制台运行await measureAll()。
2022-07-08更新
添加了ReScript (浏览器的ReasonML),新的冠军出现了!不过,我仍然不想用 ReScript 编写这个程序,因为额外的痛苦是它不仅缺少重载文字,甚至缺少类型驱动的运算符解析……
同样在今天,我收到了来自Camil Staps的Git Pull请求,添加了一个Clean实现。