击败编译器

主要观点:

  • 现代认为写汇编是徒劳的,因为编译器更优,但有传言称用汇编写解释器可超编译器。
  • 作者为 Uxn CPU 写了一个快速解释器,通过将所有操作码实现内联到Uxn::run函数中使其相对较快,比参考实现快 10 - 20%。
  • 分析汇编发现编译器存在一些低效之处,如关键值存于内存而非寄存器、调度循环为间接分支不可预测等。
  • LuaJIT 是快速解释器且用汇编写,作者开始自己写汇编优化解释器。
  • 优化措施包括将重要数据存于寄存器以避免多余加载存储、使用线程代码消除调度循环、为每个操作码写单独的实现等。
  • 还需通过 C Shims 从 Rust 调用汇编函数,处理设备 IO 时需写一些 shim 函数。
  • 测试不同实现的性能,发现手写汇编的解释器比基线快约 30%,集中式调度会显著变慢,一些其他实验未使性能提升。
  • 结论是用汇编写解释器既有趣又高效,在 Rust 中难以实现类似高性能策略,但可将汇编代码移植到 x86 - 64。

关键信息和重要细节:

  • Uxn CPU 有四种内存,评估时跟踪程序计数器pc,操作码可读写 RAM 实现自修改代码。
  • 编译器生成 256 个偏移的跳转表,通过读取特定值计算跳转目标。
  • 汇编中发现一些值的存储方式可优化,如INC操作额外加载数据栈索引。
  • LuaJIT 靠在寄存器中保存状态和间接线程提高速度。
  • 寄存器分配使用 9 个寄存器及一些临时寄存器,因 AArch64 调用约定限制需 C ABI 风格入口点。
  • 间接线程通过构建单独的跳表和宏实现跳转到下一个操作码实现。
  • 其他 255 个操作码实现主要是普通汇编代码,用宏生成代码。
  • C Shims 用于从 Rust 调用汇编函数,通过EntryHandle对象传递状态。
  • 设备 IO 通过定义 trait 处理,因与 C ABI 不兼容需写 shim 函数。
  • 性能测试用两个 CPU 密集型工作负载,发现手写汇编解释器性能提升明显,集中式调度变慢。
  • 一些未使性能提升的实验,如扩展 RAM 和使操作码实现大小相同。
  • 相关代码在 Github 上,uxn-cliuxn-gui可通过--native标志选择汇编解释器后端。
  • 帖子在 Hacker News 讨论,有人提出优化建议,如从asm!块分支进入aarch64_entry、将JUMP_TABLE移入汇编等。
阅读 9
0 条评论