V2EX = way to explore
V2EX 是一个关于分享和探索的地方
Sign Up Now
For Existing Member  Sign In
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
Mohanson
V2EX  ›  程序员

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

  •  2
     
  •   Mohanson · May 23, 2020 · 3102 views
    This topic created in 2167 days ago, the information mentioned may be changed or developed.

    为了引出 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 编译器. 稍微透露一下: 将中间语言翻译为语义等价的汇编代码.

    参考

    Supplement 1  ·  May 24, 2020
    7 replies    2020-05-24 07:02:46 +08:00
    dukiduki
        1
    dukiduki  
       May 23, 2020
    赞, 等更新.
    Liutos
        2
    Liutos  
       May 23, 2020
    大佬、大佬.jpg
    lance6716
        3
    lance6716  
       May 24, 2020 via Android
    简洁易懂,感谢分享。IR 里面 JIZ(u32)的参数是 IR 偏移量,跟别的不同,别的相当于是重复次数。就这里有一点迷惑
    wangyzj
        4
    wangyzj  
       May 24, 2020
    巧了,我也在搞编译器
    CismonX
        5
    CismonX  
       May 24, 2020
    给楼主点赞。

    前一阵子我设计了一个 Unlambda 的 IR,以及对应的编译器和 runtime,但是仍然很慢。本来想试着编译到 LLVM,但是目前还没有什么好的思路
    neoblackcap
        6
    neoblackcap  
       May 24, 2020
    其实 JIT 不需要解释器,之前的 V8 就没有解释器
    Mohanson
        7
    Mohanson  
    OP
       May 24, 2020 via Android
    @lance6716 是的,里面存的是代码偏移量
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Solana   ·   1171 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 45ms · UTC 18:12 · PVG 02:12 · LAX 11:12 · JFK 14:12
    ♥ Do have faith in what you're doing.