最近正在用 DynASM, 顺便翻译了下 DynASM 非官方文档教程. DynASM 是为 luajit 编写的 JIT 汇编预处理器和微型运行时库 (简单来讲, DynASM完成两个工作, 一个是预处理, 把你写的汇编指令 (对, 没有Elixir, DynASM并不能直接把逻辑变成汇编, 需要你手动把你的逻辑用汇编语言重写一遍, 因此性能也取决于你的汇编代码写的好坏) 变成真正的二进制机器码, 另一个是提供一个微型运行时, 来处理那些必须推迟到运行时才能执行的代码).
一些C或C++编写的语言, 甚至协议解释程序 (比如正则解释器或 json 解释器) 均可以用 DynASM JIT 化来提升性能. 比如这个项目 github.com/openresty/s… 就是 @agentzh 写的 JIT 化的正则解释器, 用于 nginx 的配置中的各种正则匹配.
本教程把 brainfuck 语言的解释器 (C语言实现) 用 DynASM 修改成了 brainfuck JIT 解释器, 性能提升了17倍 (运行Mandelbrot set测试).
说实话这个教程也不能太算得上"教程", 需要理解 DynASM 的一些基础设计, 阅读并理解 brainfuck 解释器源码, 以及适当的汇编基础和了解什么是JIT. 后续我还会翻译一些简单的教程供感兴趣的同学阅读.
repo 地址: github.com/karminski/d…
翻译版在: karminski.github.io/dynasm-doc/
原版文档在: corsix.github.io/dynasm-doc/
Introduction 简介
我们从 brainfuck 解释器开始我们的教程.
#include <stdio.h>
#include <stdlib.h>
#define TAPE_SIZE 30000
#define MAX_NESTING 100
typedef struct bf_state
{
unsigned char* tape;
unsigned char (*get_ch)(struct bf_state*);
void (*put_ch)(struct bf_state*, unsigned char);
} bf_state_t;
#define bad_program(s) exit(fprintf(stderr, "bad program near %.16s: %s\n", program, s))
static void bf_interpret(const char* program, bf_state_t* state)
{
const char* loops[MAX_NESTING];
int nloops = 0;
int n;
int nskip = 0;
unsigned char* tape_begin = state->tape - 1;
unsigned char* ptr = state->tape;
unsigned char* tape_end = state->tape + TAPE_SIZE - 1;
for(;;) {
switch(*program++) {
case '<':
for(n = 1; *program == '<'; ++n, ++program);
if(!nskip) {
ptr -= n;
while(ptr <= tape_begin)
ptr += TAPE_SIZE;
}
break;
case '>':
for(n = 1; *program == '>'; ++n, ++program);
if(!nskip) {
ptr += n;
while(ptr > tape_end)
ptr -= TAPE_SIZE;
}
break;
case '+':
for(n = 1; *program == '+'; ++n, ++program);
if(!nskip)
*ptr += n;
break;
case '-':
for(n = 1; *program == '-'; ++n, ++program);
if(!nskip)
*ptr -= n;
break;
case ',':
if(!nskip)
*ptr = state->get_ch(state);
break;
case '.':
if(!nskip)
state->put_ch(state, *ptr);
break;
case '[':
if(nloops == MAX_NESTING)
bad_program("Nesting too deep");
loops[nloops++] = program;
if(!*ptr)
++nskip;
break;
case ']':
if(nloops == 0)
bad_program("] without matching [");
if(*ptr)
program = loops[nloops-1];
else
--nloops;
if(nskip)
--nskip;
break;
case 0:
if(nloops != 0)
program = "<EOF>", bad_program("[ without matching ]");
return;
}
}
}
static void bf_putchar(bf_state_t* s, unsigned char c)
{
putchar((int)c);
}
static unsigned char bf_getchar(bf_state_t* s)
{
return (unsigned char)getchar();
}
static void bf_run(const char* program)
{
bf_state_t state;
unsigned char tape[TAPE_SIZE] = {0};
state.tape = tape;
state.get_ch = bf_getchar;
state.put_ch = bf_putchar;
bf_interpret(program, &state);
}
int main(int argc, char** argv)
{
if(argc == 2) {
long sz;
char* program;
FILE* f = fopen(argv[1], "r");
if(!f) {
fprintf(stderr, "Cannot open %s\n", argv[1]);
return 1;
}
fseek(f, 0, SEEK_END);
sz = ftell(f);
program = (char*)malloc(sz + 1);
fseek(f, 0, SEEK_SET);
program[fread(program, 1, sz, f)] = 0;
fclose(f);
bf_run(program);
return 0;
} else {
fprintf(stderr, "Usage: %s INFILE.bf\n", argv[0]);
return 1;
}
}
复制代码
我们在这个教程里, 用 DynASM 将这个 brainfuck 解释器编写成 brainfuck JIT 编译器. 来看看是否会提升运行速度.
首先, clone 这个 repo, 然后从 bf_c.c
(就是上面这一大段代码) 开始:
$ git clone https://github.com/corsix/dynasm-doc.git
$ cd dynasm-doc
$ git submodule update --init
$ cp bf_c.c tutorial.c
复制代码
我们通过运行这个程序来演示功能, 这个程序会缓慢的渲染曼德博集合(Mandelbrot set):
$ gcc -o tutorial tutorial.c
$ ./tutorial mandelbrot.bf
复制代码
(P.S. 我的CPU是 Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz, 最高 3.5GHz, 下面是输出结果, 渲染需要35.4s)
[root@m01 dynasm-doc]# time ./tutorial mandelbrot.bf
AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDEGFFEEEEDDDDDDCCCCCCCCCBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
AAAAAAAAAAAAAAABBBBBBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDEEEFGIIGFFEEEDDDDDDDDCCCCCCCCCBBBBBBBBBBBBBBBBBBBBBBBBBB
AAAAAAAAAAAAABBBBBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDEEEEFFFI KHGGGHGEDDDDDDDDDCCCCCCCCCBBBBBBBBBBBBBBBBBBBBBBB
AAAAAAAAAAAABBBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDEEEEEFFGHIMTKLZOGFEEDDDDDDDDDCCCCCCCCCBBBBBBBBBBBBBBBBBBBBB
AAAAAAAAAAABBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDEEEEEEFGGHHIKPPKIHGFFEEEDDDDDDDDDCCCCCCCCCCBBBBBBBBBBBBBBBBBB
AAAAAAAAAABBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDEEEEEEFFGHIJKS X KHHGFEEEEEDDDDDDDDDCCCCCCCCCCBBBBBBBBBBBBBBBB
AAAAAAAAABBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDEEEEEEFFGQPUVOTY ZQL[MHFEEEEEEEDDDDDDDCCCCCCCCCCCBBBBBBBBBBBBBB
AAAAAAAABBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDEEEEEFFFFFGGHJLZ UKHGFFEEEEEEEEDDDDDCCCCCCCCCCCCBBBBBBBBBBBB
AAAAAAABBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDEEEEFFFFFFGGGGHIKP KHHGGFFFFEEEEEEDDDDDCCCCCCCCCCCBBBBBBBBBBB
AAAAAAABBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDEEEEEFGGHIIHHHHHIIIJKMR VMKJIHHHGFFFFFFGSGEDDDDCCCCCCCCCCCCBBBBBBBBB
AAAAAABBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDEEEEEEFFGHK MKJIJO N R X YUSR PLV LHHHGGHIOJGFEDDDCCCCCCCCCCCCBBBBBBBB
AAAAABBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDEEEEEEEEEFFFFGH O TN S NKJKR LLQMNHEEDDDCCCCCCCCCCCCBBBBBBB
AAAAABBCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDEEEEEEEEEEEEFFFFFGHHIN Q UMWGEEEDDDCCCCCCCCCCCCBBBBBB
AAAABBCCCCCCCCCCCCCCCCCCCCCCCCCDDDDEEEEEEEEEEEEEEEFFFFFFGHIJKLOT [JGFFEEEDDCCCCCCCCCCCCCBBBBB
AAAABCCCCCCCCCCCCCCCCCCCCCCDDDDEEEEEEEEEEEEEEEEFFFFFFGGHYV RQU QMJHGGFEEEDDDCCCCCCCCCCCCCBBBB
AAABCCCCCCCCCCCCCCCCCDDDDDDDEEFJIHFFFFFFFFFFFFFFGGGGGGHIJN JHHGFEEDDDDCCCCCCCCCCCCCBBB
AAABCCCCCCCCCCCDDDDDDDDDDEEEEFFHLKHHGGGGHHMJHGGGGGGHHHIKRR UQ L HFEDDDDCCCCCCCCCCCCCCBB
AABCCCCCCCCDDDDDDDDDDDEEEEEEFFFHKQMRKNJIJLVS JJKIIIIIIJLR YNHFEDDDDDCCCCCCCCCCCCCBB
AABCCCCCDDDDDDDDDDDDEEEEEEEFFGGHIJKOU O O PR LLJJJKL OIHFFEDDDDDCCCCCCCCCCCCCCB
AACCCDDDDDDDDDDDDDEEEEEEEEEFGGGHIJMR RMLMN NTFEEDDDDDDCCCCCCCCCCCCCB
AACCDDDDDDDDDDDDEEEEEEEEEFGGGHHKONSZ QPR NJGFEEDDDDDDCCCCCCCCCCCCCC
ABCDDDDDDDDDDDEEEEEFFFFFGIPJIIJKMQ VX HFFEEDDDDDDCCCCCCCCCCCCCC
ACDDDDDDDDDDEFFFFFFFGGGGHIKZOOPPS HGFEEEDDDDDDCCCCCCCCCCCCCC
ADEEEEFFFGHIGGGGGGHHHHIJJLNY TJHGFFEEEDDDDDDDCCCCCCCCCCCCC
A PLJHGGFFEEEDDDDDDDCCCCCCCCCCCCC
ADEEEEFFFGHIGGGGGGHHHHIJJLNY TJHGFFEEEDDDDDDDCCCCCCCCCCCCC
ACDDDDDDDDDDEFFFFFFFGGGGHIKZOOPPS HGFEEEDDDDDDCCCCCCCCCCCCCC
ABCDDDDDDDDDDDEEEEEFFFFFGIPJIIJKMQ VX HFFEEDDDDDDCCCCCCCCCCCCCC
AACCDDDDDDDDDDDDEEEEEEEEEFGGGHHKONSZ QPR NJGFEEDDDDDDCCCCCCCCCCCCCC
AACCCDDDDDDDDDDDDDEEEEEEEEEFGGGHIJMR RMLMN NTFEEDDDDDDCCCCCCCCCCCCCB
AABCCCCCDDDDDDDDDDDDEEEEEEEFFGGHIJKOU O O PR LLJJJKL OIHFFEDDDDDCCCCCCCCCCCCCCB
AABCCCCCCCCDDDDDDDDDDDEEEEEEFFFHKQMRKNJIJLVS JJKIIIIIIJLR YNHFEDDDDDCCCCCCCCCCCCCBB
AAABCCCCCCCCCCCDDDDDDDDDDEEEEFFHLKHHGGGGHHMJHGGGGGGHHHIKRR UQ L HFEDDDDCCCCCCCCCCCCCCBB
AAABCCCCCCCCCCCCCCCCCDDDDDDDEEFJIHFFFFFFFFFFFFFFGGGGGGHIJN JHHGFEEDDDDCCCCCCCCCCCCCBBB
AAAABCCCCCCCCCCCCCCCCCCCCCCDDDDEEEEEEEEEEEEEEEEFFFFFFGGHYV RQU QMJHGGFEEEDDDCCCCCCCCCCCCCBBBB
AAAABBCCCCCCCCCCCCCCCCCCCCCCCCCDDDDEEEEEEEEEEEEEEEFFFFFFGHIJKLOT [JGFFEEEDDCCCCCCCCCCCCCBBBBB
AAAAABBCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDEEEEEEEEEEEEFFFFFGHHIN Q UMWGEEEDDDCCCCCCCCCCCCBBBBBB
AAAAABBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDEEEEEEEEEFFFFGH O TN S NKJKR LLQMNHEEDDDCCCCCCCCCCCCBBBBBBB
AAAAAABBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDEEEEEEFFGHK MKJIJO N R X YUSR PLV LHHHGGHIOJGFEDDDCCCCCCCCCCCCBBBBBBBB
AAAAAAABBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDEEEEEFGGHIIHHHHHIIIJKMR VMKJIHHHGFFFFFFGSGEDDDDCCCCCCCCCCCCBBBBBBBBB
AAAAAAABBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDEEEEFFFFFFGGGGHIKP KHHGGFFFFEEEEEEDDDDDCCCCCCCCCCCBBBBBBBBBBB
AAAAAAAABBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDEEEEEFFFFFGGHJLZ UKHGFFEEEEEEEEDDDDDCCCCCCCCCCCCBBBBBBBBBBBB
AAAAAAAAABBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDEEEEEEFFGQPUVOTY ZQL[MHFEEEEEEEDDDDDDDCCCCCCCCCCCBBBBBBBBBBBBBB
AAAAAAAAAABBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDEEEEEEFFGHIJKS X KHHGFEEEEEDDDDDDDDDCCCCCCCCCCBBBBBBBBBBBBBBBB
AAAAAAAAAAABBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDEEEEEEFGGHHIKPPKIHGFFEEEDDDDDDDDDCCCCCCCCCCBBBBBBBBBBBBBBBBBB
AAAAAAAAAAAABBBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDEEEEEFFGHIMTKLZOGFEEDDDDDDDDDCCCCCCCCCBBBBBBBBBBBBBBBBBBBBB
AAAAAAAAAAAAABBBBBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDEEEEFFFI KHGGGHGEDDDDDDDDDCCCCCCCCCBBBBBBBBBBBBBBBBBBBBBBB
AAAAAAAAAAAAAAABBBBBBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDEEEFGIIGFFEEEDDDDDDDDCCCCCCCCCBBBBBBBBBBBBBBBBBBBBBBBBBB
real 0m35.466s
user 0m35.462s
sys 0m0.002s
复制代码
Groundwork 基础工作
好戏上演之前, 我们先要做一些基础工作.
Includes
首先, 我们需要 #include
DynASM 的头文件:
+ #include "luajit-2.0/dynasm/dasm_proto.h"
+ #include "luajit-2.0/dynasm/dasm_x86.h"
复制代码
正如参考文档中写的, dasm_proto.h 定义 DynASM API, dasm_x86.h 则包含了上述 API 的实现 (x86/ x64).
Types
接下来, 我们将 bf_interpret
重命名为 bf_compile
, 并更改它的类型定义:
- static void bf_interpret(const char* program, bf_state_t* state)
+ static void(* bf_compile(const char* program) )(bf_state_t*)
复制代码
修改前 bf_interpret
可以接受参数 const char*
和 bf_state_t*
, 修改后的 bf_compile
只接受参数 const char*
部分, 并且返回 JIT 编译后的代码的函数指针.
bf_interpret
函数也需要修改:
- bf_interpret(program, &state);
+ bf_compile(program)(&state);
复制代码
Initialisation
搞定基础工作后, 下一步就是创建和初始化一个 DynASM state.
Variables
我们需要一个类型为 dasm_State*
的变量包含 DynASM state, 还需要两个其他的, 我们一会再解释.
并且还需要移除一个解释器变量:
- int nskip = 0;
+ dasm_State* d;
+ unsigned npc = 8;
+ unsigned nextpc = 0;
复制代码
.arch
现在我们将第一次接触 DynASM 指令, 这个是 DynASM 预处理器指令. 在这里我们定义生成目标机器码的平台架构, x86 或 x64:
+ |.if X64
+ |.arch x64
+ |.else
+ |.arch x86
+ |.endif
复制代码
开头的竖线会被 DynASM 预处理器识别. .if, .else, .endif
指令会被 DynASM 的预处理器处理, 处理方式与 C 语言预处理中的 #if, #else, #endif
相似. 执行结果就是只有一个 .arch
指令会生效.
dasm_init
我们定义了 dasm_State*
, 现在我们要分配内存空间把它装进去. 调用 dasm_init 即可:
+ |.section code
+ dasm_init(&d, DASM_MAXSECTION);
复制代码
注意跟 dasm_State**
一样, dasm_init
需要一个 integer
参数, 定义生成的机器码的 section.
我们只需要一个 code section. 所以我们传入一个参数给 .section, 这样 DynASM 预处理器就会处理成 #define DASM_MAXSECTION 1
(amongst other things), 也许给 dasm_init 传 DASM_MAXSECTION
没有直接传 1
那么直观, 但是这是个好的实践, 因为说不定将来我们就会需要更多的 section.
dasm_setupglobal
dasm_init 将会分配 dasm_State
, 但这并不是完全的初始化. 想要初始化 state 我们还需要调用几个函数. 第一个就是 dasm_setupglobal:
+ |.globals lbl_
+ void* labels[lbl__MAX];
+ dasm_setupglobal(&d, labels, lbl__MAX);
复制代码
带着参数 lbl_
的 .globals 指令会被 DynASM 预处理为一个包含一些结构的 enum
类型, 其中一个是 lbl__MAX
. 这个值必须与相同长度的 void*
数组传入到 dasm_setupglobal.
后续我们将使用 lables
数组.
dasm_setup
接下来在初始化过程调用的是 (dasm_setup)[corsix.github.io/dynasm-doc/…].
+ |.actionlist bf_actions
+ dasm_setup(&d, bf_actions);
复制代码
带 bf_actions
参数的 .actionlist
指令会被 DynASM 预处理器重写为 bf_actions
变量. 并且需要传入到 dasm_setup.
dasm_growpc
正常情况下, dasm_State
在这个节点已经完全初始化. 不过由于我们还要用动态 lables, 所以还要调用 dasm_growpc 再初始化一下:
+ dasm_growpc(&d, npc);
复制代码
我们传入了之前定义的 npc
参数, 这个参数代表动态 lable 的数量. 还有个依赖的变量叫 nextpc
是用来记录我们使用的 lable 的数量的. 这些动态 lable 将在我们编译 [, ]
时起作用.
小结
到目前为止对应的代码为:
static void(* bf_compile(const char* program) )(bf_state_t*)
{
unsigned loops[MAX_NESTING];
int nloops = 0;
int n;
dasm_State* d;
unsigned npc = 8;
unsigned nextpc = 0;
|.if X64
|.arch x64
|.else
|.arch x86
|.endif
|.section code
dasm_init(&d, DASM_MAXSECTION);
|.globals lbl_
void* labels[lbl__MAX];
dasm_setupglobal(&d, labels, lbl__MAX);
|.actionlist bf_actions
dasm_setup(&d, bf_actions);
dasm_growpc(&d, npc);
复制代码
Abstractions
在我们执行机器码之前, 先定义一些抽象(abstraction), 先定义一些让寄存器更具有意义的抽象概念:
Abstraction | Corresponding Interpreter Variable | Definition |
---|---|---|
aState | state | ebx or rbx |
aPtr | ptr | ebp or r12 |
aTapeBegin | tape_begin | esi or rsi or r13 |
aTapeEnd | tape_end | edi or rdi or r14 |
接下来再定义一些函数调用:
Abstraction | Description |
---|---|
prologue | Set up the stack frame, and set aState from the passed parameter. |
prepcall1 arg1 | Prepare to call a function with one argument, arg1. |
prepcall2 arg1, arg2 | Prepare to call a function with two arguments, arg1 and arg2. |
postcall n | Do cleanup after a call to a function with n arguments. |
epilogue | Tear down the stack frame. |
这些定义都是通过 .define (通常情况下) 或 .macro (更复杂情况下) 并且 x86, x64 POSIX, x64 Windows 下的定义也有所不同.
+ |.if X64
+ |.define aPtr, rbx
+ |.define aState, r12
+ |.if WIN
+ |.define aTapeBegin, rsi
+ |.define aTapeEnd, rdi
+ |.define rArg1, rcx
+ |.define rArg2, rdx
+ |.else
+ |.define aTapeBegin, r13
+ |.define aTapeEnd, r14
+ |.define rArg1, rdi
+ |.define rArg2, rsi
+ |.endif
+ |.macro prepcall1, arg1
+ | mov rArg1, arg1
+ |.endmacro
+ |.macro prepcall2, arg1, arg2
+ | mov rArg1, arg1
+ | mov rArg2, arg2
+ |.endmacro
+ |.define postcall, .nop
+ |.macro prologue
+ | push aPtr
+ | push aState
+ | push aTapeBegin
+ | push aTapeEnd
+ | push rax
+ | mov aState, rArg1
+ |.endmacro
+ |.macro epilogue
+ | pop rax
+ | pop aTapeEnd
+ | pop aTapeBegin
+ | pop aState
+ | pop aPtr
+ | ret
+ |.endmacro
+ |.else
+ |.define aPtr, ebx
+ |.define aState, ebp
+ |.define aTapeBegin, esi
+ |.define aTapeEnd, edi
+ |.macro prepcall1, arg1
+ | push arg1
+ |.endmacro
+ |.macro prepcall2, arg1, arg2
+ | push arg2
+ | push arg1
+ |.endmacro
+ |.macro postcall, n
+ | add esp, 4*n
+ |.endmacro
+ |.macro prologue
+ | push aPtr
+ | push aState
+ | push aTapeBegin
+ | push aTapeEnd
+ | mov aState, [esp+20]
+ |.endmacro
+ |.macro epilogue
+ | pop aTapeEnd
+ | pop aTapeBegin
+ | pop aState
+ | pop aPtr
+ | ret 4
+ |.endmacro
+ |.endif
复制代码
为 DynASM 定义了所有这些体系结构和系统有关的定义之后, 还需要检查这些为 DynASM 指定的体系结构和系统是否与 C 预处理器已知的这些是否相匹配:
+ ||#if ((defined(_M_X64) || defined(__amd64__)) != X64) || (defined(_WIN32) != WIN)
+ #error "Wrong DynASM flags used: pass `-D X64` and/or `-D WIN` to dynasm.lua as appropriate"
+ #endif
复制代码
这些以两条竖线开头的将由 DynASM 预处理器替换为 .define (同样如果有的话也可以替换为 .macro ) 但其他的不会被 DynASM 预处理器更改.
在特定情况下, 如果 x64
和\或 WIN
在 DynASM 预处理时被定义 (这里为 1
), 那么就会被替换成 1
. 如果在 DynASM 预处理时没有被定义, 那就会保持原样, 并由 C 预处理器替换为 0
.
Emitting Code
完成所有这些操作之后,我们终于可以执行一些机器码了.
Prologue
我们首先要执行的是一些初始化代码, 这些代码替换了一部分之前的解释器的代码:
- unsigned char* tape_begin = state->tape - 1;
- unsigned char* ptr = state->tape;
- unsigned char* tape_end = state->tape + TAPE_SIZE - 1;
+ |.type state, bf_state_t, aState
+ dasm_State** Dst = &d;
+ |.code
+ |->bf_main:
+ | prologue
+ | mov aPtr, state->tape
+ | lea aTapeBegin, [aPtr-1]
+ | lea aTapeEnd, [aPtr+TAPE_SIZE-1]
复制代码
我们首先看 .type 指令, 这个指令可以让我们用 state->tape
作为速记符来表达 [aState + offsetof(bf_state_t,tape)]
.
接下来这一行定义了 Dst
, 并且用 &d
初始化. 这样做是因为DynASM预处理器将把后续行重写为 dasm_put(Dst, ...)
形式的调用, 并且跟我们之前处理那些 dasm_
函数一样, 第一个参数需要是 &d
.
接下来是包含 .code
这一行, 这里指代的指令由先前的 .section code
指令引入, 并且执行的 states 需要放到 code section (这也正好是我们在处理的部分).
再之后我们定义了 label ->bf_main
. 当我们执行完机器码后, 就可以获取这个 global lable 的地址, 并且转换为函数指针.
然后, 我们调用前面定义的 prologue
宏, 执行那些指令.
最后这几行是 mov
和 lea
指令, 对应删掉的那几行解释器的代码.
像刚才说的那样, state->tape
变成操作数 mov
最终执行的是 [aState + offsetof(bf_state_t,tape)]
. 注意 offsetof(bf_state_t,tape)
和 TAPE_SIZE-1
(lea 操作数的一部分) 是所谓的编码时常量: DynASM 并不知道这是什么, 所以到 C 编译器中才会计算. 这两个值都是 C 语言中的编译时常量, 编码时常量不必是编译时常量 (稍后有例子解释).
Tape Movement
现在进入解释器阶段, 首要任务是将解释 <
部分的代码替换掉:
- if(!nskip) {
- ptr -= n;
- while(ptr <= tape_begin)
- ptr += TAPE_SIZE;
- }
+ | sub aPtr, n%TAPE_SIZE
+ | cmp aPtr, aTapeBegin
+ | ja >1
+ | add aPtr, TAPE_SIZE
+ |1:
复制代码
注意,编译器没有像解释器那样跳过代码的概念, 所以把上面的 if
部分完全删除了. ptr -= n;
和下面的循环都变成了 | sub aPtr, n%TAPE_SIZE
.
n%TAPE_SIZE
则是一个 编码阶段常量, 不是一个C编译阶段常量. DynASM 也不理解操作数的意义. 但是在这种情况下,当 bf_compile
最终运行时会计算操作数的最终值.
编译时当循环过 %TAPE_SIZE
定义的周期后, 在运行时可能仍然需要执行一次迭代, 这是因为还有 cmp, ja, add
指令. 注意语句 >1
跳转到定义 lable 1 的位置, 即 add
的下一行.
>
操作符也一样, 只不过是 add, sub
这部分倒过来:
- if(!nskip) {
- ptr += n;
- while(ptr > tape_end)
- ptr -= TAPE_SIZE;
- }
+ | add aPtr, n%TAPE_SIZE
+ | cmp aPtr, aTapeEnd
+ | jbe >1
+ | sub aPtr, TAPE_SIZE
+ |1:
复制代码
Arithmetic
接下来要改写的指令是 +
, 相对简单:
- if(!nskip)
- *ptr += n;
+ | add byte [aPtr], n
复制代码
值得注意的只有内存操作符 [aPtr]
前面的内存大小描述符 byte
. 因为内存操作数和立即操作数都不具有真实的操作数大小, 所以需要明确告知 DynASM.
请注意,我们先前使用的内存操作数不需要内存大小说明符: lea
指令并不需要, 内存操作数并不是内存访问. 并且 mov aPtr, state->tape
也不需要, 因为可以根据寄存器操作数的大小推断出内存操作数的大小. 他们是相等的.
-
指令也一样:
- if(!nskip)
- *ptr -= n;
+ | sub byte [aPtr], n
复制代码
I/O
接下来是 ,
(read char), .
(write char), 值得注意的是它们需要调用其他函数. 首先是 ,
:
- if(!nskip)
- *ptr = state->get_ch(state);
+ | prepcall1 aState
+ | call aword state->get_ch
+ | postcall 1
+ | mov byte [aPtr], al
复制代码
注意调用的抽象定义 prepcall1
和 postcall
我们之前定义过了. 同时也要注意 state->get_ch
是 [aState + offsetof(bf_state_t,get_ch)]
的速记表述, 之前介绍 .type 的时候我们说过了. 并且使用这些速记符号的时候仍然需要内存大小说明符. 内存操作数的大小不会自动推断为同等大小的 C 语言同名结构体成员. aword
(address-sized word) 说明符指的是 4 字节 (x86) 或 8 字节 (x64).
.
的转换也一样:
- if(!nskip)
- state->put_ch(state, *ptr);
+ | movzx r0, byte [aPtr]
+ | prepcall2 aState, r0
+ | call aword state->put_ch
+ | postcall 2
复制代码
注意 r0
用作寄存器操作数: 指的是 eax (x86) 或 rax (x64).
Loops
现在轮到了最有趣的指令 [, ]
. 其中 [
相当复杂:
- loops[nloops++] = program;
- if(!*ptr)
- ++nskip;
+ if(program[0] == '-' && program[1] == ']') {
+ program += 2;
+ | xor eax, eax
+ | mov byte [aPtr], al
+ } else {
+ if(nextpc == npc) {
+ npc *= 2;
+ dasm_growpc(&d, npc);
+ }
+ | cmp byte [aPtr], 0
+ | jz =>nextpc+1
+ |=>nextpc:
+ loops[nloops++] = nextpc;
+ nextpc += 2;
+ }
复制代码
首先, 我们识别指令 [, ]
并为其生成优化后的机器码. 但要排除特殊情况, 一般情况下需要两个动态标签:
一个需要从 [
跳到 ]
的后面 (之前是通过解释器中的 nskip
实现的).
另一个是从 ]
跳到 [
的后面 (之前是通过 loops
的栈实现的).
如果我们已经用了我们分配的数量的动态 lable, 还可以调用 dasm_growpc 继续分配.
然后我们发出 cmp
指令, 它的作用正如其字面意义. 如果 [aPtr]
中的 byte 是 0, 我们跳到动态 lable =>nextpc+1
(我们在稍后的 ]
操作符的逻辑中定义).
然后, 我们定义动态 lable =>nextpc
(]
需要跳回的地方). 注意 nextpc+1
和 nextpc
都是编码时常量.
然后是 ]
:
- if(*ptr)
- program = loops[nloops-1];
- else
- --nloops;
- if(nskip)
- --nskip;
+ --nloops;
+ | cmp byte [aPtr], 0
+ | jnz =>loops[nloops]
+ |=>loops[nloops]+1:
复制代码
注意条件跳转到动态 lable =>loops[nloops]
(相应的在 [
的定义是跳转到 =>nextpc
). 然后动态 lable =>loops[nloops]+1
(相应的在 [
中的定义是跳转到 jz =>nextpc+1
).
Epilogue
涵盖了所有指令之后,剩下的就是收尾并从 DynASM 中提取函数指针:
- return;
+ | epilogue
+ link_and_encode(&d);
+ dasm_free(&d);
+ return (void(*)(bf_state_t*))labels[lbl_bf_main];
复制代码
第一行调用了我们定义的 epilogue
宏. 下一行调用 link_and_encode
(一会给出), 然后调用 dasm_free, 用来释放 DynASM state. 最后, 我们将之前定义的 labels
数组传递到 dasm_setupglobal, 数组的索引是 lbl_bf_main
(由 .globals lbl_
定义, 并与全局标签 -> bf_main
对应), 并将其转换为函数指针
.
link_and_encode
函数的定义如下:
+ #if _WIN32
+ #include <Windows.h>
+ #else
+ #include <sys/mman.h>
+ #if !defined(MAP_ANONYMOUS) && defined(MAP_ANON)
+ #define MAP_ANONYMOUS MAP_ANON
+ #endif
+ #endif
+ static void* link_and_encode(dasm_State** d)
+ {
+ size_t sz;
+ void* buf;
+ dasm_link(d, &sz);
+ #ifdef _WIN32
+ buf = VirtualAlloc(0, sz, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
+ #else
+ buf = mmap(0, sz, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ #endif
+ dasm_encode(d, buf);
+ #ifdef _WIN32
+ {DWORD dwOld; VirtualProtect(buf, sz, PAGE_EXECUTE_READ, &dwOld); }
+ #else
+ mprotect(buf, sz, PROT_READ | PROT_EXEC);
+ #endif
+ return buf;
+ }
复制代码
值得注意的是 dasm_link
和 dasm_encode
调用. 其余的函数调用使用操作系统功能来分配一个 读-写 内存块, 然后将其转换为 读-执行.
注意, 我们可以分配一个 读-写-执行 内存块, 但是通常同时具有可写和可执行的内存不是好的的形式.
Compiling
根据上面的教程, 现在 tutorial.c
是这个样子的:
||#if ((defined(_M_X64) || defined(__amd64__)) != X64) || (defined(_WIN32) != WIN)
#error "Wrong DynASM flags used: pass `-D X64` and/or `-D WIN` to dynasm.lua as appropriate"
#endif
#include <stdio.h>
#include <stdlib.h>
#include "luajit-2.0/dynasm/dasm_proto.h"
#include "luajit-2.0/dynasm/dasm_x86.h"
#if _WIN32
#include <Windows.h>
#else
#include <sys/mman.h>
#if !defined(MAP_ANONYMOUS) && defined(MAP_ANON)
#define MAP_ANONYMOUS MAP_ANON
#endif
#endif
static void* link_and_encode(dasm_State** d)
{
size_t sz;
void* buf;
dasm_link(d, &sz);
#ifdef _WIN32
buf = VirtualAlloc(0, sz, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
#else
buf = mmap(0, sz, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
#endif
dasm_encode(d, buf);
#ifdef _WIN32
{DWORD dwOld; VirtualProtect(buf, sz, PAGE_EXECUTE_READ, &dwOld); }
#else
mprotect(buf, sz, PROT_READ | PROT_EXEC);
#endif
return buf;
}
#define TAPE_SIZE 30000
#define MAX_NESTING 100
typedef struct bf_state
{
unsigned char* tape;
unsigned char (*get_ch)(struct bf_state*);
void (*put_ch)(struct bf_state*, unsigned char);
} bf_state_t;
#define bad_program(s) exit(fprintf(stderr, "bad program near %.16s: %s\n", program, s))
static void(* bf_compile(const char* program) )(bf_state_t*)
{
unsigned loops[MAX_NESTING];
int nloops = 0;
int n;
dasm_State* d;
unsigned npc = 8;
unsigned nextpc = 0;
|.if X64
|.arch x64
|.else
|.arch x86
|.endif
|.section code
dasm_init(&d, DASM_MAXSECTION);
|.globals lbl_
void* labels[lbl__MAX];
dasm_setupglobal(&d, labels, lbl__MAX);
|.actionlist bf_actions
dasm_setup(&d, bf_actions);
dasm_growpc(&d, npc);
|.if X64
|.define aPtr, rbx
|.define aState, r12
|.if WIN
|.define aTapeBegin, rsi
|.define aTapeEnd, rdi
|.define rArg1, rcx
|.define rArg2, rdx
|.else
|.define aTapeBegin, r13
|.define aTapeEnd, r14
|.define rArg1, rdi
|.define rArg2, rsi
|.endif
|.macro prepcall1, arg1
| mov rArg1, arg1
|.endmacro
|.macro prepcall2, arg1, arg2
| mov rArg1, arg1
| mov rArg2, arg2
|.endmacro
|.define postcall, .nop
|.macro prologue
| push aPtr
| push aState
| push aTapeBegin
| push aTapeEnd
| push rax
| mov aState, rArg1
|.endmacro
|.macro epilogue
| pop rax
| pop aTapeEnd
| pop aTapeBegin
| pop aState
| pop aPtr
| ret
|.endmacro
|.else
|.define aPtr, ebx
|.define aState, ebp
|.define aTapeBegin, esi
|.define aTapeEnd, edi
|.macro prepcall1, arg1
| push arg1
|.endmacro
|.macro prepcall2, arg1, arg2
| push arg2
| push arg1
|.endmacro
|.macro postcall, n
| add esp, 4*n
|.endmacro
|.macro prologue
| push aPtr
| push aState
| push aTapeBegin
| push aTapeEnd
| mov aState, [esp+20]
|.endmacro
|.macro epilogue
| pop aTapeEnd
| pop aTapeBegin
| pop aState
| pop aPtr
| ret 4
|.endmacro
|.endif
|.type state, bf_state_t, aState
dasm_State** Dst = &d;
|.code
|->bf_main:
| prologue
| mov aPtr, state->tape
| lea aTapeBegin, [aPtr-1]
| lea aTapeEnd, [aPtr+TAPE_SIZE-1]
for(;;) {
switch(*program++) {
case '<':
for(n = 1; *program == '<'; ++n, ++program);
| sub aPtr, n%TAPE_SIZE
| cmp aPtr, aTapeBegin
| ja >1
| add aPtr, TAPE_SIZE
|1:
break;
case '>':
for(n = 1; *program == '>'; ++n, ++program);
| add aPtr, n%TAPE_SIZE
| cmp aPtr, aTapeEnd
| jbe >1
| sub aPtr, TAPE_SIZE
|1:
break;
case '+':
for(n = 1; *program == '+'; ++n, ++program);
| add byte [aPtr], n
break;
case '-':
for(n = 1; *program == '-'; ++n, ++program);
| sub byte [aPtr], n
break;
case ',':
| prepcall1 aState
| call aword state->get_ch
| postcall 1
| mov byte [aPtr], al
break;
case '.':
| movzx r0, byte [aPtr]
| prepcall2 aState, r0
| call aword state->put_ch
| postcall 2
break;
case '[':
if(nloops == MAX_NESTING)
bad_program("Nesting too deep");
if(program[0] == '-' && program[1] == ']') {
program += 2;
| xor eax, eax
| mov byte [aPtr], al
} else {
if(nextpc == npc) {
npc *= 2;
dasm_growpc(&d, npc);
}
| cmp byte [aPtr], 0
| jz =>nextpc+1
|=>nextpc:
loops[nloops++] = nextpc;
nextpc += 2;
}
break;
case ']':
if(nloops == 0)
bad_program("] without matching [");
--nloops;
| cmp byte [aPtr], 0
| jnz =>loops[nloops]
|=>loops[nloops]+1:
break;
case 0:
if(nloops != 0)
program = "<EOF>", bad_program("[ without matching ]");
| epilogue
link_and_encode(&d);
dasm_free(&d);
return (void(*)(bf_state_t*))labels[lbl_bf_main];
}
}
}
static void bf_putchar(bf_state_t* s, unsigned char c)
{
putchar((int)c);
}
static unsigned char bf_getchar(bf_state_t* s)
{
return (unsigned char)getchar();
}
static void bf_run(const char* program)
{
bf_state_t state;
unsigned char tape[TAPE_SIZE] = {0};
state.tape = tape;
state.get_ch = bf_getchar;
state.put_ch = bf_putchar;
bf_compile(program)(&state);
}
int main(int argc, char** argv)
{
if(argc == 2) {
long sz;
char* program;
FILE* f = fopen(argv[1], "r");
if(!f) {
fprintf(stderr, "Cannot open %s\n", argv[1]);
return 1;
}
fseek(f, 0, SEEK_END);
sz = ftell(f);
program = (char*)malloc(sz + 1);
fseek(f, 0, SEEK_SET);
program[fread(program, 1, sz, f)] = 0;
fclose(f);
bf_run(program);
return 0;
} else {
fprintf(stderr, "Usage: %s INFILE.bf\n", argv[0]);
return 1;
}
}
复制代码
如果没跟上, 还可以从这里获取代码:
$ git clone https://github.com/corsix/dynasm-doc.git
$ cd dynasm-doc
$ git submodule update --init
$ cp bf_dynasm.c tutorial.c
复制代码
为了编译 tutorial.c
, 我们首先需要通过 DynASM 预处理程序运行它. 预处理器是用 Lua 编写的, 因此我们首先编译一个 minimal Lua 解释器 (如果有luajit也可以直接用luajit运行dynasm.lua, 就可以省略这一步):
$ gcc -o minilua luajit-2.0/src/host/minilua.c
复制代码
然后运行 DynASM 预处理器:
$ ./minilua luajit-2.0/dynasm/dynasm.lua -o tutorial.posix64.c -D X64 tutorial.c
复制代码
完成预处理后, 调用 C 编译器:
$ gcc -o tutorial tutorial.posix64.c
复制代码
然后, 我们可以运行生成的可执行文件, 该可执行文件将很快运行 Mandelbrot set:
$ ./tutorial mandelbrot.bf
复制代码
(我的运行结果, 2.129s, 源程序是 35.466s, 耗时是原来的 6%, 性能提升了17倍)
[root@m01 dynasm-doc]# time ./tutorial mandelbrot.bf
AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDEGFFEEEEDDDDDDCCCCCCCCCBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
AAAAAAAAAAAAAAABBBBBBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDEEEFGIIGFFEEEDDDDDDDDCCCCCCCCCBBBBBBBBBBBBBBBBBBBBBBBBBB
AAAAAAAAAAAAABBBBBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDEEEEFFFI KHGGGHGEDDDDDDDDDCCCCCCCCCBBBBBBBBBBBBBBBBBBBBBBB
AAAAAAAAAAAABBBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDEEEEEFFGHIMTKLZOGFEEDDDDDDDDDCCCCCCCCCBBBBBBBBBBBBBBBBBBBBB
AAAAAAAAAAABBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDEEEEEEFGGHHIKPPKIHGFFEEEDDDDDDDDDCCCCCCCCCCBBBBBBBBBBBBBBBBBB
AAAAAAAAAABBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDEEEEEEFFGHIJKS X KHHGFEEEEEDDDDDDDDDCCCCCCCCCCBBBBBBBBBBBBBBBB
AAAAAAAAABBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDEEEEEEFFGQPUVOTY ZQL[MHFEEEEEEEDDDDDDDCCCCCCCCCCCBBBBBBBBBBBBBB
AAAAAAAABBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDEEEEEFFFFFGGHJLZ UKHGFFEEEEEEEEDDDDDCCCCCCCCCCCCBBBBBBBBBBBB
AAAAAAABBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDEEEEFFFFFFGGGGHIKP KHHGGFFFFEEEEEEDDDDDCCCCCCCCCCCBBBBBBBBBBB
AAAAAAABBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDEEEEEFGGHIIHHHHHIIIJKMR VMKJIHHHGFFFFFFGSGEDDDDCCCCCCCCCCCCBBBBBBBBB
AAAAAABBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDEEEEEEFFGHK MKJIJO N R X YUSR PLV LHHHGGHIOJGFEDDDCCCCCCCCCCCCBBBBBBBB
AAAAABBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDEEEEEEEEEFFFFGH O TN S NKJKR LLQMNHEEDDDCCCCCCCCCCCCBBBBBBB
AAAAABBCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDEEEEEEEEEEEEFFFFFGHHIN Q UMWGEEEDDDCCCCCCCCCCCCBBBBBB
AAAABBCCCCCCCCCCCCCCCCCCCCCCCCCDDDDEEEEEEEEEEEEEEEFFFFFFGHIJKLOT [JGFFEEEDDCCCCCCCCCCCCCBBBBB
AAAABCCCCCCCCCCCCCCCCCCCCCCDDDDEEEEEEEEEEEEEEEEFFFFFFGGHYV RQU QMJHGGFEEEDDDCCCCCCCCCCCCCBBBB
AAABCCCCCCCCCCCCCCCCCDDDDDDDEEFJIHFFFFFFFFFFFFFFGGGGGGHIJN JHHGFEEDDDDCCCCCCCCCCCCCBBB
AAABCCCCCCCCCCCDDDDDDDDDDEEEEFFHLKHHGGGGHHMJHGGGGGGHHHIKRR UQ L HFEDDDDCCCCCCCCCCCCCCBB
AABCCCCCCCCDDDDDDDDDDDEEEEEEFFFHKQMRKNJIJLVS JJKIIIIIIJLR YNHFEDDDDDCCCCCCCCCCCCCBB
AABCCCCCDDDDDDDDDDDDEEEEEEEFFGGHIJKOU O O PR LLJJJKL OIHFFEDDDDDCCCCCCCCCCCCCCB
AACCCDDDDDDDDDDDDDEEEEEEEEEFGGGHIJMR RMLMN NTFEEDDDDDDCCCCCCCCCCCCCB
AACCDDDDDDDDDDDDEEEEEEEEEFGGGHHKONSZ QPR NJGFEEDDDDDDCCCCCCCCCCCCCC
ABCDDDDDDDDDDDEEEEEFFFFFGIPJIIJKMQ VX HFFEEDDDDDDCCCCCCCCCCCCCC
ACDDDDDDDDDDEFFFFFFFGGGGHIKZOOPPS HGFEEEDDDDDDCCCCCCCCCCCCCC
ADEEEEFFFGHIGGGGGGHHHHIJJLNY TJHGFFEEEDDDDDDDCCCCCCCCCCCCC
A PLJHGGFFEEEDDDDDDDCCCCCCCCCCCCC
ADEEEEFFFGHIGGGGGGHHHHIJJLNY TJHGFFEEEDDDDDDDCCCCCCCCCCCCC
ACDDDDDDDDDDEFFFFFFFGGGGHIKZOOPPS HGFEEEDDDDDDCCCCCCCCCCCCCC
ABCDDDDDDDDDDDEEEEEFFFFFGIPJIIJKMQ VX HFFEEDDDDDDCCCCCCCCCCCCCC
AACCDDDDDDDDDDDDEEEEEEEEEFGGGHHKONSZ QPR NJGFEEDDDDDDCCCCCCCCCCCCCC
AACCCDDDDDDDDDDDDDEEEEEEEEEFGGGHIJMR RMLMN NTFEEDDDDDDCCCCCCCCCCCCCB
AABCCCCCDDDDDDDDDDDDEEEEEEEFFGGHIJKOU O O PR LLJJJKL OIHFFEDDDDDCCCCCCCCCCCCCCB
AABCCCCCCCCDDDDDDDDDDDEEEEEEFFFHKQMRKNJIJLVS JJKIIIIIIJLR YNHFEDDDDDCCCCCCCCCCCCCBB
AAABCCCCCCCCCCCDDDDDDDDDDEEEEFFHLKHHGGGGHHMJHGGGGGGHHHIKRR UQ L HFEDDDDCCCCCCCCCCCCCCBB
AAABCCCCCCCCCCCCCCCCCDDDDDDDEEFJIHFFFFFFFFFFFFFFGGGGGGHIJN JHHGFEEDDDDCCCCCCCCCCCCCBBB
AAAABCCCCCCCCCCCCCCCCCCCCCCDDDDEEEEEEEEEEEEEEEEFFFFFFGGHYV RQU QMJHGGFEEEDDDCCCCCCCCCCCCCBBBB
AAAABBCCCCCCCCCCCCCCCCCCCCCCCCCDDDDEEEEEEEEEEEEEEEFFFFFFGHIJKLOT [JGFFEEEDDCCCCCCCCCCCCCBBBBB
AAAAABBCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDEEEEEEEEEEEEFFFFFGHHIN Q UMWGEEEDDDCCCCCCCCCCCCBBBBBB
AAAAABBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDEEEEEEEEEFFFFGH O TN S NKJKR LLQMNHEEDDDCCCCCCCCCCCCBBBBBBB
AAAAAABBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDEEEEEEFFGHK MKJIJO N R X YUSR PLV LHHHGGHIOJGFEDDDCCCCCCCCCCCCBBBBBBBB
AAAAAAABBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDEEEEEFGGHIIHHHHHIIIJKMR VMKJIHHHGFFFFFFGSGEDDDDCCCCCCCCCCCCBBBBBBBBB
AAAAAAABBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDEEEEFFFFFFGGGGHIKP KHHGGFFFFEEEEEEDDDDDCCCCCCCCCCCBBBBBBBBBBB
AAAAAAAABBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDEEEEEFFFFFGGHJLZ UKHGFFEEEEEEEEDDDDDCCCCCCCCCCCCBBBBBBBBBBBB
AAAAAAAAABBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDEEEEEEFFGQPUVOTY ZQL[MHFEEEEEEEDDDDDDDCCCCCCCCCCCBBBBBBBBBBBBBB
AAAAAAAAAABBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDEEEEEEFFGHIJKS X KHHGFEEEEEDDDDDDDDDCCCCCCCCCCBBBBBBBBBBBBBBBB
AAAAAAAAAAABBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDEEEEEEFGGHHIKPPKIHGFFEEEDDDDDDDDDCCCCCCCCCCBBBBBBBBBBBBBBBBBB
AAAAAAAAAAAABBBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDEEEEEFFGHIMTKLZOGFEEDDDDDDDDDCCCCCCCCCBBBBBBBBBBBBBBBBBBBBB
AAAAAAAAAAAAABBBBBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDEEEEFFFI KHGGGHGEDDDDDDDDDCCCCCCCCCBBBBBBBBBBBBBBBBBBBBBBB
AAAAAAAAAAAAAAABBBBBBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCDDDDDDDDDDEEEFGIIGFFEEEDDDDDDDDCCCCCCCCCBBBBBBBBBBBBBBBBBBBBBBBBBB
real 0m2.129s
user 0m2.126s
sys 0m0.003s
复制代码