Dđộ roll khi back up source code là gì năm 2024

Mình đang viết một ngôn ngữ lập trình scripting và rất chú trọng về hiệu năng, cũng như tốc độ xử lý của nó. Bài viết này là một phần kinh nghiệm đã trãi qua khi xử lý phần virtual machine cho ngôn ngữ.

Trong bài viết này, mình sẽ đi chi tiết từ cách tạo một máy ảo (virtual machine) thực thi bytecode của một ngôn ngữ lập trình, cho đến từng cách tối ưu nó trên các trình biên dịch phổ biến (cross-compiler).

Bài viết này có thể sẽ hữu ích đối với những ai đam mê nghiên cứu về viết ngôn ngữ lập trình hay sử dụng virtual machine cho mục đích nào đó, viết giả lập chẳng hạn.

Bytecode

Bytecode, còn được gọi là portable code hoặc p-code, là cách thức lưu trữ dạng mã các chỉ thị (instructions - các opcode) trong lập trình máy tính, được thiết kế để phần mềm thông dịch thực hiện hiệu quả trên nền tảng máy ảo - theo Wiki

Nói dễ hiểu hơn thì bytecode là một loạt chỉ thị được tạo sau khi biên dịch source code, sau đó đưa chúng vào máy ảo để thực thi.

Hiện tại, thì opcode instruction thường được biểu diễn theo 2 dạng

32bit

Theo kiểu nguyên mẫu của ISA, giống với các chỉ thị được sử dụng trên CPU thật. Gói gọn trong 32bit đó, sẽ chứa cả opcode, các tham số phụ như dấu, đối số khác, địa chỉ register...

Từ Lua 5.0, người ta đã chuyển sang sử dụng register-based và 32bit instruction, có thể thấy nó trong The Implementation of Lua 5.0 và Lua 5.0 source code.

Dđộ roll khi back up source code là gì năm 2024

  • 6bit đầu chứa opcode, 8bit cho arg A, 18bit còn lại chia đều cho B và C
  • 18bit cuối cũng dành cho Bx, để biểu diễn số hạng đơn lớn hơn, sBx cho số có dấu.

8bit (1 byte đơn)

Phương pháp này tương đối ít gặp hơn, nhưng có ưu điểm là không tốn nhiều byte như ở trên, là sao?

  • Ví dụ bạn có một opcode NOTHING với mục đích là không cần làm gì cả. Và khi đọc đến nó thì máy ảo chẳng làm gì, cứ next đến opcode tiếp theo. Như vậy, nếu dùng 32bit (4 byte) thì có phải là thừa đến tận 3 byte.
  • Nhưng cũng có tí nhược điểm là phải dùng nhiều mã hơn để biểu diễn trong một số trường hợp, chẳng hạn muốn có số nguyên 64bit, thì phải cần đến 8 byte tiếp theo.

Ta dễ dàng bắt gặp nó trong ebook .

Dđộ roll khi back up source code là gì năm 2024

  • `
    ...  

else

typedef union {

struct {  
    OpCode op :  6; // OP  |  
    u32       :  8; // A   |  
    u32   arg : 18; // Bx  |  
};                  //     v  
u32 n;              // -> 32bit  
} Inst;

endif
0 chỉ cần 1 byte
  • ` ...
# else typedef union { struct { OpCode op : 6; // OP | u32 : 8; // A | u32 arg : 18; // Bx | }; // v u32 n; // -> 32bit } Inst; # endif

1 thì cần thêm 1 byte để lấy vị trí đã lưu của constant (giới hạn là 256 constant, nếu muốn tăng thêm thì cần 1 byte hoặc hơn).

Virtual machine

(Phần này mình chỉ nói nhanh qua)

Máy ảo ở đây là môi trường được xây dựng trên máy thật, về hình thức thì nó giống như mô phỏng CPU hay máy thật và được dùng để thực thi các chỉ thị đưa vào (bytecode).

Trong ngôn ngữ lập trình có sử dụng máy ảo, nó thường được xây dựng theo 2 kiểu

  • Registers based (các thanh ghi), giống như một CPU vậy, thường thấy trong JVM và Lua VM.
  • Stack based (mô hình ngăn xếp), thay vì giới hạn nghiêm ngặt số thanh ghi chứa giá trị, thì cách này rộng hơn và cũng tốn nhiều tài nguyên hơn, bù lại các giá trị có thể lưu trữ lâu dài hơn trên stack ảo.

Triển khai máy ảo

Mục đích của máy ảo trong post là tương tác với hai giá trị constant và value (đơn giản chỉ là số nguyên), thông qua các chỉ thị đã thiết lập sẵn, kết thúc chương trình sẽ trả về value.

Phần này mình chỉ sử dụng ngôn ngữ C đơn giản vì 2 lý do:

  • Ngôn ngữ cấp hệ thống, dễ dàng tương tác với bộ nhớ
  • Hầu hết các máy ảo cần hiệu năng cao đều viết bằng C

C++ cũng không ngoại lệ, nhưng chỉ C là đủ rồi.

Khai báo


# include 

# include 
typedef uint8_t  u8;    // for opcode
typedef uint16_t u16;   //   _ arg
typedef uint32_t u32;   //   _ instruction
typedef uint64_t u64;   //   _ value, constant

  • Đầu tiên, ta cần một số type cơ bản
  • Chỉ sử dụng standard library nên có thể chạy trên nhiều nền tảng

Xây đựng mã chỉ thị

Mình sẽ cho các chỉ thị vào một

    ...

# else
typedef union {
    struct {
        OpCode op :  6; // OP  |
        u32       :  8; // A   | 
        u32   arg : 18; // Bx  |
    };                  //     v
    u32 n;              // -> 32bit
} Inst;

# endif

2 như sau

typedef enum {
    OP_HLT = 0,         // return value
    OP_LDK = 1,         // constant = constants[arg]
    OP_ADD = 2,         // value += constant
    OP_SUB = 3,         // value -= constant
    OP_LEQ = 4          // if value <= constant, pc -= arg
} OpCode;

  • `
    ...  

else

typedef union {

struct {  
    OpCode op :  6; // OP  |  
    u32       :  8; // A   |  
    u32   arg : 18; // Bx  |  
};                  //     v  
u32 n;              // -> 32bit  
} Inst;

endif
3 (halt), dừng chương trình và trả về value
  • ` ...
# else typedef union { struct { OpCode op : 6; // OP | u32 : 8; // A | u32 arg : 18; // Bx | }; // v u32 n; // -> 32bit } Inst; # endif

4 (load constant), load một giá trị constant

  • `
    ...  

else

typedef union {

struct {  
    OpCode op :  6; // OP  |  
    u32       :  8; // A   |  
    u32   arg : 18; // Bx  |  
};                  //     v  
u32 n;              // -> 32bit  
} Inst;

endif
5 (add), cộng value với constant
  • ` ...
# else typedef union { struct { OpCode op : 6; // OP | u32 : 8; // A | u32 arg : 18; // Bx | }; // v u32 n; // -> 32bit } Inst; # endif

6 (subtract), trừ value cho constant

  • `
    ...  

else

typedef union {

struct {  
    OpCode op :  6; // OP  |  
    u32       :  8; // A   |  
    u32   arg : 18; // Bx  |  
};                  //     v  
u32 n;              // -> 32bit  
} Inst;

endif
7 (less than or equal), nếu value bé hơn hoặc bằng constant thì jump đến một vị trí trong bytecode (nhằm mục đích tạo vòng lặp, kéo dài chương trình)

Tiếp theo là instruction, mình sẽ dùng macro để viết theo 2 cách biểu diễn bytecode đã nói ở trên.

  • 1 byte đơn

ifdef OPCODE_BYTE

typedef u8 Inst;

...

  • và 32bit, dựa trên instruction layout của Lua
...

else

typedef union {

struct {
    OpCode op :  6; // OP  |
    u32       :  8; // A   | 
    u32   arg : 18; // Bx  |
};                  //     v
u32 n;              // -> 32bit
} Inst;

endif


Đến phần emit code, mình tiếp tục dùng macro.

ifdef OPCODE_BYTE

define EMIT_HLT() (OP_HLT)

define EMIT_LDK(k) (OP_LDK), ((k) & 0xff), (((k) >> 8) & 0xff)

define EMIT_ADD() (OP_ADD)

define EMIT_SUB() (OP_SUB)

define EMIT_LEQ(n) (OP_LEQ), ((n) & 0xff), (((n) >> 8) & 0xff)
...

  • ` ...
# else typedef union { struct { OpCode op : 6; // OP | u32 : 8; // A | u32 arg : 18; // Bx | }; // v u32 n; // -> 32bit } Inst; # endif

8 và

    ...  

# else  
typedef union {  
    struct {  
        OpCode op :  6; // OP  |  
        u32       :  8; // A   |  
        u32   arg : 18; // Bx  |  
    };                  //     v  
    u32 n;              // -> 32bit  
} Inst;  

# endif  
9 đều có 1 tham số, và mình dùng số nguyên 16bit

  • Ở trên sẽ gộp 2 byte lại, còn bên dưới thì cho nó vào field 18bit

    ...

# else

# define EMIT_HLT()  ((Inst) { .op = OP_HLT })

# define EMIT_LDK(k) ((Inst) { .op = OP_LDK, .arg = (k) })

# define EMIT_ADD()  ((Inst) { .op = OP_ADD })

# define EMIT_SUB()  ((Inst) { .op = OP_SUB })

# define EMIT_LEQ(n) ((Inst) { .op = OP_LEQ, .arg = (n) })

# endif

Phần cốt lõi của máy ảo

Đây là hàm để thực thi code

static u64 exec(Inst *code, u64 *consts) {
    const Inst *pc;
    u64 value, constant;
    ...

  • `

ifdef OPCODE_BYTE

define EMIT_HLT() (OP_HLT)

define EMIT_LDK(k) (OP_LDK), ((k) & 0xff), (((k) >> 8) & 0xff)

define EMIT_ADD() (OP_ADD)

define EMIT_SUB() (OP_SUB)

define EMIT_LEQ(n) (OP_LEQ), ((n) & 0xff), (((n) >> 8) & 0xff)
...  
`

0, bytecode hay các chỉ thị đầu vào

  • `

ifdef OPCODE_BYTE

define EMIT_HLT() (OP_HLT)

define EMIT_LDK(k) (OP_LDK), ((k) & 0xff), (((k) >> 8) & 0xff)

define EMIT_ADD() (OP_ADD)

define EMIT_SUB() (OP_SUB)

define EMIT_LEQ(n) (OP_LEQ), ((n) & 0xff), (((n) >> 8) & 0xff)
...  
`

1, mảng các giá trị, ở đầy mình dùng số nguyên 64bit

  • `

ifdef OPCODE_BYTE

define EMIT_HLT() (OP_HLT)

define EMIT_LDK(k) (OP_LDK), ((k) & 0xff), (((k) >> 8) & 0xff)

define EMIT_ADD() (OP_ADD)

define EMIT_SUB() (OP_SUB)

define EMIT_LEQ(n) (OP_LEQ), ((n) & 0xff), (((n) >> 8) & 0xff)
...  
`

2, program counter, trỏ đến chỉ thị hiện tại

  • `

ifdef OPCODE_BYTE

define EMIT_HLT() (OP_HLT)

define EMIT_LDK(k) (OP_LDK), ((k) & 0xff), (((k) >> 8) & 0xff)

define EMIT_ADD() (OP_ADD)

define EMIT_SUB() (OP_SUB)

define EMIT_LEQ(n) (OP_LEQ), ((n) & 0xff), (((n) >> 8) & 0xff)
...  
`

3 và


# ifdef OPCODE_BYTE  

# define EMIT_HLT()  (OP_HLT)  

# define EMIT_LDK(k) (OP_LDK), ((k) & 0xff), (((k) >> 8) & 0xff)  

# define EMIT_ADD()  (OP_ADD)  

# define EMIT_SUB()  (OP_SUB)  

# define EMIT_LEQ(n) (OP_LEQ), ((n) & 0xff), (((n) >> 8) & 0xff)  
    ...  
4 sẽ giống như hai register

  • ifdef OPCODE_BYTE

    define EMIT_HLT() (OP_HLT)

    define EMIT_LDK(k) (OP_LDK), ((k) & 0xff), (((k) >> 8) & 0xff)

    define EMIT_ADD() (OP_ADD)

    define EMIT_SUB() (OP_SUB)

    define EMIT_LEQ(n) (OP_LEQ), ((n) & 0xff), (((n) >> 8) & 0xff)
       ...  
    

    3 - biến lưu giá trị chính, cũng là giá trị trả về của hàm
  • ifdef OPCODE_BYTE

    define EMIT_HLT() (OP_HLT)

    define EMIT_LDK(k) (OP_LDK), ((k) & 0xff), (((k) >> 8) & 0xff)

    define EMIT_ADD() (OP_ADD)

    define EMIT_SUB() (OP_SUB)

    define EMIT_LEQ(n) (OP_LEQ), ((n) & 0xff), (((n) >> 8) & 0xff)
       ...  
    

    4 - sẽ chứa giá trị sau khi load từ mảng

    ifdef OPCODE_BYTE

    define EMIT_HLT() (OP_HLT)

    define EMIT_LDK(k) (OP_LDK), ((k) & 0xff), (((k) >> 8) & 0xff)

    define EMIT_ADD() (OP_ADD)

    define EMIT_SUB() (OP_SUB)

    define EMIT_LEQ(n) (OP_LEQ), ((n) & 0xff), (((n) >> 8) & 0xff)
       ...  
    

    1

Tiếp theo là khởi tạo giá trị

    ...
    value = 0;
    constant = 0;
    pc = code;

Phần đọc code của máy ảo sẽ là một vòng lặp, khi đọc đến các opcode sẽ rẻ nhánh. Mình sẽ dùng


# ifdef OPCODE_BYTE

# define EMIT_HLT()  (OP_HLT)

# define EMIT_LDK(k) (OP_LDK), ((k) & 0xff), (((k) >> 8) & 0xff)

# define EMIT_ADD()  (OP_ADD)

# define EMIT_SUB()  (OP_SUB)

# define EMIT_LEQ(n) (OP_LEQ), ((n) & 0xff), (((n) >> 8) & 0xff)
    ...

8 và


# ifdef OPCODE_BYTE

# define EMIT_HLT()  (OP_HLT)

# define EMIT_LDK(k) (OP_LDK), ((k) & 0xff), (((k) >> 8) & 0xff)

# define EMIT_ADD()  (OP_ADD)

# define EMIT_SUB()  (OP_SUB)

# define EMIT_LEQ(n) (OP_LEQ), ((n) & 0xff), (((n) >> 8) & 0xff)
    ...

9, nhưng để tiện cho các phần sau của bài viết, nên mình chuyển chúng sang macro

    ...

# define DISPATCH() for (;;) switch(GET_OP())

# define CODE(op)   case op

# define NEXT()     continue

  • `
    ...  

else

define EMIT_HLT() ((Inst) { .op = OP_HLT })

define EMIT_LDK(k) ((Inst) { .op = OP_LDK, .arg = (k) })

define EMIT_ADD() ((Inst) { .op = OP_ADD })

define EMIT_SUB() ((Inst) { .op = OP_SUB })

define EMIT_LEQ(n) ((Inst) { .op = OP_LEQ, .arg = (n) })

endif
0 sẽ rẽ nhánh theo opcode  
  • ... # else # define EMIT_HLT() ((Inst) { .op = OP_HLT }) # define EMIT_LDK(k) ((Inst) { .op = OP_LDK, .arg = (k) }) # define EMIT_ADD() ((Inst) { .op = OP_ADD }) # define EMIT_SUB() ((Inst) { .op = OP_SUB }) # define EMIT_LEQ(n) ((Inst) { .op = OP_LEQ, .arg = (n) }) # endif 1 cho vòng lặp vô hạn
  • ... # else # define EMIT_HLT() ((Inst) { .op = OP_HLT }) # define EMIT_LDK(k) ((Inst) { .op = OP_LDK, .arg = (k) }) # define EMIT_ADD() ((Inst) { .op = OP_ADD }) # define EMIT_SUB() ((Inst) { .op = OP_SUB }) # define EMIT_LEQ(n) ((Inst) { .op = OP_LEQ, .arg = (n) }) # endif 2 sẽ lấy opcode từ instruction, một lát sẽ biết nó là gì
  • động từ dispatch có nghĩa là gửi công văn đi, chỉ đạo thực hiện ![](https://i0.wp.com/twemoji.maxcdn.com/v/14.0.2/72x72/1f603.png))
    • ` ...
# else # define EMIT_HLT() ((Inst) { .op = OP_HLT }) # define EMIT_LDK(k) ((Inst) { .op = OP_LDK, .arg = (k) }) # define EMIT_ADD() ((Inst) { .op = OP_ADD }) # define EMIT_SUB() ((Inst) { .op = OP_SUB }) # define EMIT_LEQ(n) ((Inst) { .op = OP_LEQ, .arg = (n) }) # endif

3 để định nghĩa nhánh, thông qua opcode nhập vào

  • `
    ...  

else

define EMIT_HLT() ((Inst) { .op = OP_HLT })

define EMIT_LDK(k) ((Inst) { .op = OP_LDK, .arg = (k) })

define EMIT_ADD() ((Inst) { .op = OP_ADD })

define EMIT_SUB() ((Inst) { .op = OP_SUB })

define EMIT_LEQ(n) ((Inst) { .op = OP_LEQ, .arg = (n) })

endif
4 để đọc tiếp  
  • ... # else # define EMIT_HLT() ((Inst) { .op = OP_HLT }) # define EMIT_LDK(k) ((Inst) { .op = OP_LDK, .arg = (k) }) # define EMIT_ADD() ((Inst) { .op = OP_ADD }) # define EMIT_SUB() ((Inst) { .op = OP_SUB }) # define EMIT_LEQ(n) ((Inst) { .op = OP_LEQ, .arg = (n) }) # endif 5 để trực tiếp trở về # ifdef OPCODE_BYTE # define EMIT_HLT() (OP_HLT) # define EMIT_LDK(k) (OP_LDK), ((k) & 0xff), (((k) >> 8) & 0xff) # define EMIT_ADD() (OP_ADD) # define EMIT_SUB() (OP_SUB) # define EMIT_LEQ(n) (OP_LEQ), ((n) & 0xff), (((n) >> 8) & 0xff) ... 8 (thay vì qua 2 bước nếu dùng ... # else # define EMIT_HLT() ((Inst) { .op = OP_HLT }) # define EMIT_LDK(k) ((Inst) { .op = OP_LDK, .arg = (k) }) # define EMIT_ADD() ((Inst) { .op = OP_ADD }) # define EMIT_SUB() ((Inst) { .op = OP_SUB }) # define EMIT_LEQ(n) ((Inst) { .op = OP_LEQ, .arg = (n) }) # endif 7)
Đi tiếp các nhánh opcode, đầu tiên là
...

else

define EMIT_HLT() ((Inst) { .op = OP_HLT })

define EMIT_LDK(k) ((Inst) { .op = OP_LDK, .arg = (k) })

define EMIT_ADD() ((Inst) { .op = OP_ADD })

define EMIT_SUB() ((Inst) { .op = OP_SUB })

define EMIT_LEQ(n) ((Inst) { .op = OP_LEQ, .arg = (n) })

endif


8
...
DISPATCH() {
    CODE(OP_HLT): {
        return value;   // returns away
    }

tiếp theo, 
...

else

typedef union {

struct {
    OpCode op :  6; // OP  |
    u32       :  8; // A   | 
    u32   arg : 18; // Bx  |
};                  //     v
u32 n;              // -> 32bit
} Inst;

endif


5 và 
...

else

typedef union {

struct {
    OpCode op :  6; // OP  |
    u32       :  8; // A   | 
    u32   arg : 18; // Bx  |
};                  //     v
u32 n;              // -> 32bit
} Inst;

endif


6
typedef enum {
OP_HLT = 0,         // return value
OP_LDK = 1,         // constant = constants[arg]
OP_ADD = 2,         // value += constant
OP_SUB = 3,         // value -= constant
OP_LEQ = 4          // if value <= constant, pc -= arg
} OpCode;

0
  • ` ...
# else # define EMIT_HLT() ((Inst) { .op = OP_HLT }) # define EMIT_LDK(k) ((Inst) { .op = OP_LDK, .arg = (k) }) # define EMIT_ADD() ((Inst) { .op = OP_ADD }) # define EMIT_SUB() ((Inst) { .op = OP_SUB }) # define EMIT_LEQ(n) ((Inst) { .op = OP_LEQ, .arg = (n) }) # endif
4 để đi tiếp, phần
    ...  

# else  

# define EMIT_HLT()  ((Inst) { .op = OP_HLT })  

# define EMIT_LDK(k) ((Inst) { .op = OP_LDK, .arg = (k) })  

# define EMIT_ADD()  ((Inst) { .op = OP_ADD })  

# define EMIT_SUB()  ((Inst) { .op = OP_SUB })  

# define EMIT_LEQ(n) ((Inst) { .op = OP_LEQ, .arg = (n) })  

# endif  
8 không có vì nó trả về kết quả luôn

tiếp theo nữa là

    ...

# else
typedef union {
    struct {
        OpCode op :  6; // OP  |
        u32       :  8; // A   | 
        u32   arg : 18; // Bx  |
    };                  //     v
    u32 n;              // -> 32bit
} Inst;

# endif

4

typedef enum {
    OP_HLT = 0,         // return value
    OP_LDK = 1,         // constant = constants[arg]
    OP_ADD = 2,         // value += constant
    OP_SUB = 3,         // value -= constant
    OP_LEQ = 4          // if value <= constant, pc -= arg
} OpCode;

1

  • static u64 exec(Inst *code, u64 *consts) {
    const Inst *pc;  
    u64 value, constant;  
    ...  
    
    4 sẽ lấy tham số theo sau opcode trong instruction
  • vị trí static u64 exec(Inst *code, u64 *consts) {
    const Inst *pc;  
    u64 value, constant;  
    ...  
    
    5 là nơi lưu giá trị constant đã định nghĩa ban đầu

...và cuối cùng là

    ...

# else
typedef union {
    struct {
        OpCode op :  6; // OP  |
        u32       :  8; // A   | 
        u32   arg : 18; // Bx  |
    };                  //     v
    u32 n;              // -> 32bit
} Inst;

# endif

7

typedef enum {
    OP_HLT = 0,         // return value
    OP_LDK = 1,         // constant = constants[arg]
    OP_ADD = 2,         // value += constant
    OP_SUB = 3,         // value -= constant
    OP_LEQ = 4          // if value <= constant, pc -= arg
} OpCode;

2

  • roll `

ifdef OPCODE_BYTE

define EMIT_HLT() (OP_HLT)

define EMIT_LDK(k) (OP_LDK), ((k) & 0xff), (((k) >> 8) & 0xff)

define EMIT_ADD() (OP_ADD)

define EMIT_SUB() (OP_SUB)

define EMIT_LEQ(n) (OP_LEQ), ((n) & 0xff), (((n) >> 8) & 0xff)
...  
`

2 ngược về

static u64 exec(Inst *code, u64 *consts) {  
    const Inst *pc;  
    u64 value, constant;  
    ...  
8 đơn vị (hay jump back
static u64 exec(Inst *code, u64 *consts) {  
    const Inst *pc;  
    u64 value, constant;  
    ...  
8 đơn vị)

Xử lý nốt hai macro

    ...
    value = 0;
    constant = 0;
    pc = code;

0 và

static u64 exec(Inst *code, u64 *consts) {
    const Inst *pc;
    u64 value, constant;
    ...

4 nữa là có thể chạy được.

Do hai instruction layout khác nhau nên phải chia làm 2 phần, đặt code dưới vào sau phần khởi tạo giá trị

typedef enum {
    OP_HLT = 0,         // return value
    OP_LDK = 1,         // constant = constants[arg]
    OP_ADD = 2,         // value += constant
    OP_SUB = 3,         // value -= constant
    OP_LEQ = 4          // if value <= constant, pc -= arg
} OpCode;

3

  • ...  
    value = 0;  
    constant = 0;  
    pc = code;  
    

    0, lấy giá trị (opcode) và tăng

    `

ifdef OPCODE_BYTE

define EMIT_HLT() (OP_HLT)

define EMIT_LDK(k) (OP_LDK), ((k) & 0xff), (((k) >> 8) & 0xff)

define EMIT_ADD() (OP_ADD)

define EMIT_SUB() (OP_SUB)

define EMIT_LEQ(n) (OP_LEQ), ((n) & 0xff), (((n) >> 8) & 0xff)
...  
`

2 lên 1

  • static u64 exec(Inst *code, u64 *consts) {
    const Inst *pc;  
    u64 value, constant;  
    ...  
    

    4, tăng trước 2, sau đó

    ...  
    value = 0;  
    constant = 0;  
    pc = code;  
    
    5 hai giá trị byte trước

typedef enum {
    OP_HLT = 0,         // return value
    OP_LDK = 1,         // constant = constants[arg]
    OP_ADD = 2,         // value += constant
    OP_SUB = 3,         // value -= constant
    OP_LEQ = 4          // if value <= constant, pc -= arg
} OpCode;

4

  • phần này dễ sử dụng hơn, do chúng nằm gọn trong struct
  • chỗ static u64 exec(Inst *code, u64 *consts) {
    const Inst *pc;  
    u64 value, constant;  
    ...  
    

    4 có

    ...  
    value = 0;  
    constant = 0;  
    pc = code;  
    

    7 vì trước đó đã tăng

    `

ifdef OPCODE_BYTE

define EMIT_HLT() (OP_HLT)

define EMIT_LDK(k) (OP_LDK), ((k) & 0xff), (((k) >> 8) & 0xff)

define EMIT_ADD() (OP_ADD)

define EMIT_SUB() (OP_SUB)

define EMIT_LEQ(n) (OP_LEQ), ((n) & 0xff), (((n) >> 8) & 0xff)
...  
`

2 lên rồi (để ý chỗ

    ...  

# else  

# define EMIT_HLT()  ((Inst) { .op = OP_HLT })  

# define EMIT_LDK(k) ((Inst) { .op = OP_LDK, .arg = (k) })  

# define EMIT_ADD()  ((Inst) { .op = OP_ADD })  

# define EMIT_SUB()  ((Inst) { .op = OP_SUB })  

# define EMIT_LEQ(n) ((Inst) { .op = OP_LEQ, .arg = (n) })  

# endif  
0 ở trên sẽ thấy, sau đó mới đến
    ...  

# else  

# define EMIT_HLT()  ((Inst) { .op = OP_HLT })  

# define EMIT_LDK(k) ((Inst) { .op = OP_LDK, .arg = (k) })  

# define EMIT_ADD()  ((Inst) { .op = OP_ADD })  

# define EMIT_SUB()  ((Inst) { .op = OP_SUB })  

# define EMIT_LEQ(n) ((Inst) { .op = OP_LEQ, .arg = (n) })  

# endif  
4)

Vậy là xong toàn bộ máy ảo, đến phần không thể thiếu của một chương trình C - main

typedef enum {
    OP_HLT = 0,         // return value
    OP_LDK = 1,         // constant = constants[arg]
    OP_ADD = 2,         // value += constant
    OP_SUB = 3,         // value -= constant
    OP_LEQ = 4          // if value <= constant, pc -= arg
} OpCode;

5

Mà khoan, phải xác định rõ sẽ cho máy ảo thực thi cái gì trước đã. Mình nghĩ ra mẫu code đơn giản sau

typedef enum {
    OP_HLT = 0,         // return value
    OP_LDK = 1,         // constant = constants[arg]
    OP_ADD = 2,         // value += constant
    OP_SUB = 3,         // value -= constant
    OP_LEQ = 4          // if value <= constant, pc -= arg
} OpCode;

6

  • gán `
    ...  

define DISPATCH() for (;;) switch(GET_OP())

define CODE(op) case op

define NEXT() continue
1
  • set một label ` ...
# define DISPATCH() for (;;) switch(GET_OP()) # define CODE(op) case op # define NEXT() continue

2

  • giảm `
    ...  

define DISPATCH() for (;;) switch(GET_OP())

define CODE(op) case op

define NEXT() continue
3 đi  
...  

define DISPATCH() for (;;) switch(GET_OP())

define CODE(op) case op

define NEXT() continue
4
  • tăng ` ...
# define DISPATCH() for (;;) switch(GET_OP()) # define CODE(op) case op # define NEXT() continue

3 thêm

    ...  

# define DISPATCH() for (;;) switch(GET_OP())  

# define CODE(op)   case op  

# define NEXT()     continue  
6

  • nếu `
    ...  

define DISPATCH() for (;;) switch(GET_OP())

define CODE(op) case op

define NEXT() continue
7 thì  
...  

define DISPATCH() for (;;) switch(GET_OP())

define CODE(op) case op

define NEXT() continue
8

Vậy là ta sẽ có 3 giá trị trong constant

typedef enum {

OP_HLT = 0,         // return value
OP_LDK = 1,         // constant = constants[arg]
OP_ADD = 2,         // value += constant
OP_SUB = 3,         // value -= constant
OP_LEQ = 4          // if value <= constant, pc -= arg
} OpCode;

7

Tiếp theo là emit code
typedef enum {
OP_HLT = 0,         // return value
OP_LDK = 1,         // constant = constants[arg]
OP_ADD = 2,         // value += constant
OP_SUB = 3,         // value -= constant
OP_LEQ = 4          // if value <= constant, pc -= arg
} OpCode;

8
  • Chỗ ` ...
# define DISPATCH() for (;;) switch(GET_OP()) # define CODE(op) case op # define NEXT() continue
9 nên cho gì vào? Phải tính toán thôi.

Ta để ý chỗ

    ...
    DISPATCH() {
        CODE(OP_HLT): {
            return value;   // returns away
        }

0 ở trên, sẽ có

    ...
    DISPATCH() {
        CODE(OP_HLT): {
            return value;   // returns away
        }

1,

static u64 exec(Inst *code, u64 *consts) {
    const Inst *pc;
    u64 value, constant;
    ...

8 chính là

    ...

# define DISPATCH() for (;;) switch(GET_OP())

# define CODE(op)   case op

# define NEXT()     continue

9 ta cần tìm.

Trong mảng


# ifdef OPCODE_BYTE

# define EMIT_HLT()  (OP_HLT)

# define EMIT_LDK(k) (OP_LDK), ((k) & 0xff), (((k) >> 8) & 0xff)

# define EMIT_ADD()  (OP_ADD)

# define EMIT_SUB()  (OP_SUB)

# define EMIT_LEQ(n) (OP_LEQ), ((n) & 0xff), (((n) >> 8) & 0xff)
    ...

0 ở trên, vị trí tương ứng của label

    ...

# define DISPATCH() for (;;) switch(GET_OP())

# define CODE(op)   case op

# define NEXT()     continue

2 (trong code mẫu) chính là

    ...
    DISPATCH() {
        CODE(OP_HLT): {
            return value;   // returns away
        }

6. Vậy ta chỉ cần tìm khoảng cách giữa

    ...
    DISPATCH() {
        CODE(OP_HLT): {
            return value;   // returns away
        }

7 và

    ...
    DISPATCH() {
        CODE(OP_HLT): {
            return value;   // returns away
        }

8. Có thể thấy rõ, một thằng ở vị trí

    ...
    DISPATCH() {
        CODE(OP_HLT): {
            return value;   // returns away
        }

9 và một thằng ở vị trí

    ...

# define DISPATCH() for (;;) switch(GET_OP())

# define CODE(op)   case op

# define NEXT()     continue

6, ở trên có

typedef enum {
    OP_HLT = 0,         // return value
    OP_LDK = 1,         // constant = constants[arg]
    OP_ADD = 2,         // value += constant
    OP_SUB = 3,         // value -= constant
    OP_LEQ = 4          // if value <= constant, pc -= arg
} OpCode;

01 nên sẽ điền vào là

    ...

# define DISPATCH() for (;;) switch(GET_OP())

# define CODE(op)   case op

# define NEXT()     continue

4 (tức

typedef enum {
    OP_HLT = 0,         // return value
    OP_LDK = 1,         // constant = constants[arg]
    OP_ADD = 2,         // value += constant
    OP_SUB = 3,         // value -= constant
    OP_LEQ = 4          // if value <= constant, pc -= arg
} OpCode;

03).

Nhưng đừng vội, hãy nhìn

    ...

# else
typedef union {
    struct {
        OpCode op :  6; // OP  |
        u32       :  8; // A   | 
        u32   arg : 18; // Bx  |
    };                  //     v
    u32 n;              // -> 32bit
} Inst;

# endif

8 và

    ...

# else
typedef union {
    struct {
        OpCode op :  6; // OP  |
        u32       :  8; // A   | 
        u32   arg : 18; // Bx  |
    };                  //     v
    u32 n;              // -> 32bit
} Inst;

# endif

9 (phần

typedef enum {
    OP_HLT = 0,         // return value
    OP_LDK = 1,         // constant = constants[arg]
    OP_ADD = 2,         // value += constant
    OP_SUB = 3,         // value -= constant
    OP_LEQ = 4          // if value <= constant, pc -= arg
} OpCode;

06), xem là nó được định nghĩa như thế nào? Có đến 2 dấu phẩy, tức không phải 1 mà là 3 giá trị đấy!

Quay trở lại, ban đầu chỉ là

    ...

# define DISPATCH() for (;;) switch(GET_OP())

# define CODE(op)   case op

# define NEXT()     continue

4, nhưng nếu có define

typedef enum {
    OP_HLT = 0,         // return value
    OP_LDK = 1,         // constant = constants[arg]
    OP_ADD = 2,         // value += constant
    OP_SUB = 3,         // value -= constant
    OP_LEQ = 4          // if value <= constant, pc -= arg
} OpCode;

06 thì sẽ là

typedef enum {
    OP_HLT = 0,         // return value
    OP_LDK = 1,         // constant = constants[arg]
    OP_ADD = 2,         // value += constant
    OP_SUB = 3,         // value -= constant
    OP_LEQ = 4          // if value <= constant, pc -= arg
} OpCode;

09 (4 cộng thêm (2 nhân 3), vì có 3

    ...

# else
typedef union {
    struct {
        OpCode op :  6; // OP  |
        u32       :  8; // A   | 
        u32   arg : 18; // Bx  |
    };                  //     v
    u32 n;              // -> 32bit
} Inst;

# endif

4, tức

typedef enum {
    OP_HLT = 0,         // return value
    OP_LDK = 1,         // constant = constants[arg]
    OP_ADD = 2,         // value += constant
    OP_SUB = 3,         // value -= constant
    OP_LEQ = 4          // if value <= constant, pc -= arg
} OpCode;

11), vậy mình thêm macro vào đây

typedef enum {
    OP_HLT = 0,         // return value
    OP_LDK = 1,         // constant = constants[arg]
    OP_ADD = 2,         // value += constant
    OP_SUB = 3,         // value -= constant
    OP_LEQ = 4          // if value <= constant, pc -= arg
} OpCode;

9

Test thử

Ok, giờ có thể chạy để test 2 phương pháp bytecode. Mình test trên

typedef enum {
    OP_HLT = 0,         // return value
    OP_LDK = 1,         // constant = constants[arg]
    OP_ADD = 2,         // value += constant
    OP_SUB = 3,         // value -= constant
    OP_LEQ = 4          // if value <= constant, pc -= arg
} OpCode;

12

Các case test đều sử dụng tùy chọn tối ưu hóa, kết quả theo format

typedef enum {
    OP_HLT = 0,         // return value
    OP_LDK = 1,         // constant = constants[arg]
    OP_ADD = 2,         // value += constant
    OP_SUB = 3,         // value -= constant
    OP_LEQ = 4          // if value <= constant, pc -= arg
} OpCode;

13 opcode, tính theo giây.


# ifdef OPCODE_BYTE
typedef u8 Inst;
    ...

0 platforms gcc -O3 msvc /O2 clang -O3 Linux x64 (WLS) 2.49s \ 2.60s _ 2.71s \ 1.85s Windows 32bit 2.90s \ 3.27s 2.36s \ 2.75s _ Windows 64bit 2.96s \ 2.95s 2.39s \ 2.15s 2.27s \ 2.37s

Kết quả trên cho thấy rằng, 2 phương pháp bytecode không hơn kém nhau bao nhiêu, mà còn phụ thuộc khá nhiều vào compiler.

Và tất nhiên là bài viết vẫn chưa kết thúc đâu, đến phần tiếp theo thôi nào!

Tối ưu nhẹ phần runtime

Khi chuyển code trên sang assembly (thông qua Godbolt), ta sẽ thấy vài chỗ khác biệt nhỏ giữa GCC và MSVC, đây cũng là điểm khiến GCC chậm hơn trong code trên


# ifdef OPCODE_BYTE
typedef u8 Inst;
    ...

1

  • typedef enum {

    OP_HLT = 0,         // return value  
    OP_LDK = 1,         // constant = constants[arg]  
    OP_ADD = 2,         // value += constant  
    OP_SUB = 3,         // value -= constant  
    OP_LEQ = 4          // if value <= constant, pc -= arg  
    
    } OpCode;

    14 không đáng kể, nếu dùng option -O2, -O3
  • typedef enum {

    OP_HLT = 0,         // return value  
    OP_LDK = 1,         // constant = constants[arg]  
    OP_ADD = 2,         // value += constant  
    OP_SUB = 3,         // value -= constant  
    OP_LEQ = 4          // if value <= constant, pc -= arg  
    
    } OpCode;

    15 trong MSVC đều sử dụng short (2 byte) cho tất cả near label, còn GCC thì khác, đều là

    typedef enum {

    OP_HLT = 0,         // return value  
    OP_LDK = 1,         // constant = constants[arg]  
    OP_ADD = 2,         // value += constant  
    OP_SUB = 3,         // value -= constant  
    OP_LEQ = 4          // if value <= constant, pc -= arg  
    
    } OpCode;

    16 hoặc

    typedef enum {

    OP_HLT = 0,         // return value  
    OP_LDK = 1,         // constant = constants[arg]  
    OP_ADD = 2,         // value += constant  
    OP_SUB = 3,         // value -= constant  
    OP_LEQ = 4          // if value <= constant, pc -= arg  
    
    } OpCode;

    17 (x86_64).
  • `

ifdef OPCODE_BYTE

define EMIT_HLT() (OP_HLT)

define EMIT_LDK(k) (OP_LDK), ((k) & 0xff), (((k) >> 8) & 0xff)

define EMIT_ADD() (OP_ADD)

define EMIT_SUB() (OP_SUB)

define EMIT_LEQ(n) (OP_LEQ), ((n) & 0xff), (((n) >> 8) & 0xff)
...  
`

9 hoạt động gần giống như các

typedef enum {  
    OP_HLT = 0,         // return value  
    OP_LDK = 1,         // constant = constants[arg]  
    OP_ADD = 2,         // value += constant  
    OP_SUB = 3,         // value -= constant  
    OP_LEQ = 4          // if value <= constant, pc -= arg  
} OpCode;  
19 nối nhau, đồng nghĩa sẽ có rất nhiều
typedef enum {  
    OP_HLT = 0,         // return value  
    OP_LDK = 1,         // constant = constants[arg]  
    OP_ADD = 2,         // value += constant  
    OP_SUB = 3,         // value -= constant  
    OP_LEQ = 4          // if value <= constant, pc -= arg  
} OpCode;  
15 được sử dụng.

Để hạn chế điều này, có một trick nhỏ đó là sử dụng computed goto (tạm dịch: bước đi có tính toán). Cách này chỉ có thể sử dụng trên được trên GCC và Clang compiler.

Về cơ bản thì địa chỉ của các label sẽ được đưa vào một mảng, truy xuất mảng đó thông qua index để lấy địa chỉ và jump đến label tương ứng. Ta thực hiện với máy ảo trên như sau:


# ifdef OPCODE_BYTE
typedef u8 Inst;
    ...

2

  • Ngay đoạn

    typedef enum {

    OP_HLT = 0,         // return value  
    OP_LDK = 1,         // constant = constants[arg]  
    OP_ADD = 2,         // value += constant  
    OP_SUB = 3,         // value -= constant  
    OP_LEQ = 4          // if value <= constant, pc -= arg  
    
    } OpCode;

    21, ta chỉ cần thêm vào trước đó một

    typedef enum {

    OP_HLT = 0,         // return value  
    OP_LDK = 1,         // constant = constants[arg]  
    OP_ADD = 2,         // value += constant  
    OP_SUB = 3,         // value -= constant  
    OP_LEQ = 4          // if value <= constant, pc -= arg  
    
    } OpCode;

    22 để xác định compiler có phải là GCC hay Clang.


# ifdef OPCODE_BYTE
typedef u8 Inst;
    ...

3

  • typedef enum {

    OP_HLT = 0,         // return value  
    OP_LDK = 1,         // constant = constants[arg]  
    OP_ADD = 2,         // value += constant  
    OP_SUB = 3,         // value -= constant  
    OP_LEQ = 4          // if value <= constant, pc -= arg  
    
    } OpCode;

    23 sẽ là array chứa các địa chỉ của label


# ifdef OPCODE_BYTE
typedef u8 Inst;
    ...

4

  • Dùng

    typedef enum {

    OP_HLT = 0,         // return value  
    OP_LDK = 1,         // constant = constants[arg]  
    OP_ADD = 2,         // value += constant  
    OP_SUB = 3,         // value -= constant  
    OP_LEQ = 4          // if value <= constant, pc -= arg  
    
    } OpCode;

    24 để lấy địa chỉ của label
  • Các address được add vào tất nhiên phải theo thứ tự của `
    ...  

else

typedef union {

struct {  
    OpCode op :  6; // OP  |  
    u32       :  8; // A   |  
    u32   arg : 18; // Bx  |  
};                  //     v  
u32 n;              // -> 32bit  
} Inst;

endif
2

Vậy là xong, ta có thể test ngay, kết quả nhanh hơn trước rất nhiều.

ifdef OPCODE_BYTE

typedef u8 Inst;

...

5

ifdef OPCODE_BYTE

typedef u8 Inst;

...
`

6

Có một câu hỏi đặt ra, có thể áp dụng cách này trên MSVC được không?

Về cách code thì không thể vì dính ngay syntax error, nhưng về cách thức hoạt động thì có thể code giống như vậy bằng assembly (x86 có thể dễ dàng sử dụng inline assembler, còn x64 thì không).