跳至主要內容
  • Hostloc 空間訪問刷分
  • 售賣場
  • 廣告位
  • 賣站?

4563博客

全新的繁體中文 WordPress 網站
  • 首頁
  • 手写 JIT 编译器, 三天时间能学会吗(狗头, 第二天)?
未分類
26 5 月 2020

手写 JIT 编译器, 三天时间能学会吗(狗头, 第二天)?

手写 JIT 编译器, 三天时间能学会吗(狗头, 第二天)?

資深大佬 : Mohanson 1

  • 第一天: https://v2ex.com/t/674528
  • 第二天: 本文即是
  • 第三天: 5 月 24 号晚上会写

为了引出 JIT 即时编译器, 我们得先有一个解释器, skaven yes yes,

Brainfuck 解释器与 IR 优化

Brainfuck 是一种简单且最小的图灵完备编程语言. 这种语言由八种运算符构成:

  • “>”, 指针加一
  • “<“, 指针减一
  • “+”, 指针指向的字节的值加一
  • “-“, 指针指向的字节的值减一
  • “.”, 输出指针指向的单元内容(ASCII 码)
  • “,”, 输入内容到指针指向的单元(ASCII 码)
  • “[“, 如果指针指向的单元值为零,向后跳转到对应的 ] 指令的次一指令处
  • “]”, 如果指针指向的单元值不为零,向前跳转到对应的 [ 指令的次一指令处

Brainfuck 完全模仿了图灵纸带机, 后者则是计算机的老祖宗. 理论上一切能被计算的问题都能通过 Brainfuck 被计算(注: 有些问题是不能被计算的, 对于这些问题现代计算机也无能无力).

我会使用 Rust 来编写这个解释器并省略了一部分无关紧要的代码, 以使得核心逻辑清晰.

定义一个枚举类型 Opcode 来代表以上的 8 个字符, 然后编写一个转换函数将字节转换为 Opcode. 由于 “[” 与 “]” 总是成双成对的出现且互相关联, 代码额外使用了 jtable 来存储它们之间的位置关系, 以便快速决定跳转的位置.

#[derive(Debug, PartialEq)] pub enum Opcode {     SHR = 0x3E,     SHL = 0x3C,     ADD = 0x2B,     SUB = 0x2D,     PUTCHAR = 0x2E,     GETCHAR = 0x2C,     LB = 0x5B,     RB = 0x5D, }  pub struct Code {     pub instrs: Vec<Opcode>,     pub jtable: std::collections::HashMap<usize, usize>, }  impl Code {     pub fn from(data: Vec<u8>) -> Result<Self, Box<dyn std::error::Error>> {         // transform bytes to opcodes         // ...     } } 

再拿到 Opcode 数组之后, 便可以编写针对 Opcode 解释器. Brainfuck 的执行需要首先定义一个栈, 一个栈指针 SP, 源码以及计数器 PC.

struct Interpreter {     stack: Vec<u8>, }  impl Interpreter {     fn run(&mut self, data: Vec<u8>) -> Result<(), Box<dyn std::error::Error>> {         let code = Code::from(data)?;         let code_len = code.instrs.len();         let mut pc = 0;         let mut ps = 0;         loop {             if pc >= code_len {                 break;             }             match code.instrs[pc] {                 Opcode::SHL => ps = if ps == 0 { 0 } else { ps - 1 },                 Opcode::SHR => {                     ps += 1;                     if ps == self.stack.len() {                         self.stack.push(0)                     }                 }                 Opcode::ADD => {                     self.stack[ps] = self.stack[ps].overflowing_add(1).0;                 }                 Opcode::SUB => {                     self.stack[ps] = self.stack[ps].overflowing_sub(1).0;                 }                 Opcode::PUTCHAR => {                     std::io::stdout().write_all(&[self.stack[ps]])?;                 }                 Opcode::GETCHAR => {                     let mut buf: Vec<u8> = vec![0; 1];                     std::io::stdin().read_exact(&mut buf)?;                     self.stack[ps] = buf[0];                 }                 Opcode::LB => {                     if self.stack[ps] == 0x00 {                         pc = code.jtable[&pc];                     }                 }                 Opcode::RB => {                     if self.stack[ps] != 0x00 {                         pc = code.jtable[&pc];                     }                 }             }             pc += 1;         }         Ok(())     } } 

Hello World!

下面是一个在屏幕上打印 Hello World! 的程序.

++++++++++[>+++++++>++++++++++>+++>+<<<<-] >++.>+.+++++++..+++.>++.<<+++++++++++++++. >.+++.------.--------.>+.>. 

我不太清楚上古的程序员们是如何写出这份代码的, 不过我也不在乎…毕竟能运行不是吗?

IR 与优化

目前为止, 我们已经有了一个能正常跑的解释器, 但我对上面的代码并不满意, 如果你仔细观察, 可以发现 Brainfuck 源代码中存在着大量冗余. 让我们将 Hello World 的代码以 Opcode 的形式打印出来:

[     ADD, ADD, ADD, ADD, ADD, ADD, ADD, ADD, ADD, ADD, LB, SHR, ADD, ADD, ADD, ADD,     ADD, ADD, ADD, SHR, ADD, ADD, ADD, ADD, ADD, ADD, ADD, ADD, ADD, ADD, SHR, ADD,     ADD, ADD, SHR, ADD, SHL, SHL, SHL, SHL, SUB, RB, SHR, ADD, ADD, PUTCHAR, SHR,     ADD, PUTCHAR, ADD, ADD, ADD, ADD, ADD, ADD, ADD, PUTCHAR, PUTCHAR, ADD, ADD, ADD,     PUTCHAR, SHR, ADD, ADD, PUTCHAR, SHL, SHL, ADD, ADD, ADD, ADD, ADD, ADD, ADD,     ADD, ADD, ADD, ADD, ADD, ADD, ADD, ADD, PUTCHAR, SHR, PUTCHAR, ADD, ADD, ADD,     PUTCHAR, SUB, SUB, SUB, SUB, SUB, SUB, PUTCHAR, SUB, SUB, SUB, SUB, SUB, SUB,     SUB, SUB, PUTCHAR, SHR, ADD, PUTCHAR, SHR, PUTCHAR ] 

如果希望解释器执行的稍微快一点, 可以对相邻的相同操作符进行折叠操作, 比如以 ADD(10) 来表示连续存储的十个 ADD 操作符. 为此定义如下的中间表示.

中间语言(英语: Intermediate Language, IR), 在计算机科学中, 是指一种应用于抽象机器(abstract machine)的编程语言, 它设计的目的, 是用来帮助我们分析计算机程序. 这个术语源自于编译器, 在编译器将源代码编译为目的码的过程中, 会先将源代码转换为一个或多个的中间表述, 以方便编译器进行最佳化, 并产生出目的机器的机器语言.

#[derive(Debug, PartialEq)] pub enum IR {     SHR(u32),     SHL(u32),     ADD(u8),     SUB(u8),     PUTCHAR,     GETCHAR,     JIZ(u32), // JUMP If Zero     JNZ(u32), // JUMP If Not Zero } 

好吧, 让我们直接给出 Hello World! 程序的 IR 表示:

[     ADD(10), JIZ(12), SHR(1), ADD(7), SHR(1), ADD(10), SHR(1), ADD(3), SHR(1),     ADD(1), SHL(4), SUB(1), JNZ(1), SHR(1), ADD(2), PUTCHAR, SHR(1), ADD(1), PUTCHAR,     ADD(7), PUTCHAR, PUTCHAR, ADD(3), PUTCHAR, SHR(1), ADD(2), PUTCHAR, SHL(2),     ADD(15), PUTCHAR, SHR(1), PUTCHAR, ADD(3), PUTCHAR, SUB(6), PUTCHAR, SUB(8),     PUTCHAR, SHR(1), ADD(1), PUTCHAR, SHR(1), PUTCHAR ] 

我们可以针对此中间语言编写解释器(相信你应该已经知道该怎么做了). 在测试中, 基于中间语言的解释器大概要比原始解释器快 5 倍左右.

那么, 明天的文章中将会介绍如何针对该中间语言编写 JIT 编译器. 稍微透露一下: 将中间语言翻译为语义等价的汇编代码.

参考

  • [1] 中间语言, 维基百科, https://zh.wikipedia.org/zh-hans/中間語言
大佬有話說 (7)

  • 資深大佬 : dukiduki

    赞, 等更新.

  • 資深大佬 : Liutos

    大佬、大佬.jpg

  • 資深大佬 : lance6716

    简洁易懂,感谢分享。IR 里面 JIZ(u32)的参数是 IR 偏移量,跟别的不同,别的相当于是重复次数。就这里有一点迷惑

  • 資深大佬 : wangyzj

    巧了,我也在搞编译器

  • 資深大佬 : CismonX

    给主点赞。

    前一阵子我设计了一个 Unlambda 的 IR,以及对应的编译器和 runtime,但是仍然很慢。本来想试着编译到 LLVM,但是目前还没有什么好的思路

  • 資深大佬 : neoblackcap

    其实 JIT 不需要解释器,之前的 V8 就没有解释器

  • 主 資深大佬 : Mohanson

    @lance6716 是的,里面存的是代码偏移量

文章導覽

上一篇文章
下一篇文章

AD

其他操作

  • 登入
  • 訂閱網站內容的資訊提供
  • 訂閱留言的資訊提供
  • WordPress.org 台灣繁體中文

51la

4563博客

全新的繁體中文 WordPress 網站
返回頂端
本站採用 WordPress 建置 | 佈景主題採用 GretaThemes 所設計的 Memory
4563博客
  • Hostloc 空間訪問刷分
  • 售賣場
  • 廣告位
  • 賣站?
在這裡新增小工具