410 likes | 864 Views
Mini C 到 Unicore 32 的编译和模拟. 徐泽骅,刘智猷,寻云波. 成果. Mini C 到 Unicore 32 平台的编译器 生成 Unicore 32 汇编 比 gcc 不开优化的结果快 20% Unicore 32 模拟器 行为级模拟 Cache 周期数估算. 任务分工. 徐泽骅:Mini C 编译器前端 刘智猷:Mini C 编译器后端 寻云波:Unicore 32 模拟器. Mini C 编译器前端. 徐泽骅. Mini C 编译器前端结构. 类型系统. 对象类型 标量类型 基本类型 一级指针 集合类型:数组 函数类型
E N D
Mini C 到 Unicore 32 的编译和模拟 徐泽骅,刘智猷,寻云波
成果 • Mini C 到 Unicore 32 平台的编译器 • 生成 Unicore 32 汇编 • 比 gcc 不开优化的结果快 20% • Unicore 32 模拟器 • 行为级模拟 • Cache • 周期数估算
任务分工 • 徐泽骅:Mini C 编译器前端 • 刘智猷:Mini C 编译器后端 • 寻云波:Unicore 32 模拟器
Mini C 编译器前端 徐泽骅
类型系统 • 对象类型 • 标量类型 • 基本类型 • 一级指针 • 集合类型:数组 • 函数类型 • <返回值, 参数列表>:都是标量类型 • 没有指针数组和高阶指针 • 没有数组指针和函数指针
语言支持 • 声明管理 • 符号表 • 参数列表管理 • 局部状态管理 • ID 分配及导出 • 变量 ID • 标号
AST 的构造和求值 • Bison 解释器自底向上构造 AST • 组合表达式和语句 • 类型推导 • 规约到最顶端的语句列表时,自顶向下求值 • 语句(表达式)递归调用其子句(子表达式),并生成控制逻辑 • 自顶向下求值避免了回填操作
前端优化 • 没有流图? • 实际上 AST 就是流图 • 没有 goto、break 和 continue • 逻辑短路相当于复合 if
前端优化 • 在特定的执行状态中求值 • 定义一个特殊的结构,称为“执行状态” • 每个求值过程都接受一个执行状态作为参数 • 执行状态影响求值的行为 • 求值的过程会改变执行状态
前端优化 • 在执行状态中记录可用表达式,就实现了公共表达式优化 • 在公共表达式的基础上通过三次迭代,可以实现循环常量外提 • 第一次迭代:可用表达式积累 • 第二次迭代:可变表达式消除 • 消除的结果就是循环常量 • 第三次迭代:生成最终代码
int func(int *a, int b, int c){ int i; for(i = 0; i < 10; ++i) a[b * c] = 0;} LD_WORD TEMP_2 [AUTO_2 + 0*0] LD_WORD TEMP_3 [AUTO_1 + 0*0] MUL TEMP_4 TEMP_3 TEMP_2 LD_WORD TEMP_5 [AUTO_0 + 0*0] LABEL_1: MOV_ALL TEMP_12 0 ST_WORD TEMP_12 [TEMP_5 + TEMP_4*4] ADD TEMP_8 TEMP_1 1 ST_WORD TEMP_8 [AUTO_3 + 0*0] LD_WORD TEMP_1 [AUTO_3 + 0*0] CSUB TEMP_0 TEMP_1 10 JMP_SLT LABEL_1 前端优化
IR to Assembly 刘智猷
要解决的问题 • 需要保证中间代码中Call/Return语句的语义 • 维护不同过程的栈帧 • 按调用约定保存和恢复寄存器 • 将数量无限的临时变量映射到有限个寄存器上 • 基于图染色算法的寄存器分配
Interference Graph t2 t1 = 1 t2 = 2 t3 = t1 + t2 t4 = t1 + 3 t5 = t1 + t2 t1 t3 t5 t4 t1-t5 are not live Slides make by Wei Li ,in Standford CS243
Register Assignment t2/r2 t1/r1 t3/r3 t5/r1 t4/r3 Slides make by Wei Li ,in Standford CS243
另外一些小问题 • 32位定长指令系统,32位地址空间 • 不能给寄存器赋值一个静态变量的指针 • 在代码段中预先保存需要用的静态变量指针 • 不能直接将一个寄存器赋值为大整数 • 用Mov和位移凑 • 对指令中的立即数大小有限制 • 为不能处理的立即数分配寄存器
MiniC vs MiniJava:优化视角 • 优点 • 没有继承 • 新问题:存在指针,潜在地修改其他变量的值。 int *p; int a, b, c, d; if ( some_cond) p = &a; else p = &b; *p = a + b;
MiniC vs MiniJava:优化视角 • 另一个问题:中间表示中使用保守的Load/Store策略:每个MiniC中的变量存储在一块地址空间中,更新变量时Load到Temp变量上,计算,再存回。 • 产生大量的Load/Store指令
MiniC vs MiniJava:优化视角 void swap(int* a, int* b) { int tmp; tmp = *a; *a = *b; *b = tmp; } ST_WORD TEMP_-1 [AUTO_0 + 0*0] ST_WORD TEMP_-2 [AUTO_1 + 0*0] LD_WORD TEMP_1 [AUTO_0 + 0*0] LD_WORD TEMP_0 [TEMP_1 + 0*0] ST_WORD TEMP_0 [AUTO_2 + 0*0] LD_WORD TEMP_3 [AUTO_1 + 0*0] LD_WORD TEMP_2 [TEMP_3 + 0*0] ST_WORD TEMP_2 [TEMP_1 + 0*0] LD_WORD TEMP_4 [AUTO_2 + 0*0] ST_WORD TEMP_4 [TEMP_3 + 0*0] RETURN
别名分析 • 对C语言中的指针: 分析每个Temp变量在每个点上做为指针可能指向的变量 • 对于多余的Load/Store: 分析每个Temp变量在每个点上一定存有的变量. • 还应考虑数组:指针和数组有同样的表示。例如:int*
别名分析 • 为每个变量定义三个属性 • hold:该变量在当前点一定存储着的变量 • ref:该变量做为指针可能指向的变量 • offseted:该变量做为指针是否做过偏移(即是否可能指向数组变量的非0元素) • 使用数据流分析方法计算这三个变量
别名分析-例子 • Mov temp1, temp2 • temp1.hold = temp2.hold • temp1.ref = temp2.ref • temp1.offseted = temp2.offseted • Add temp1, var2, var3 • temp1.hold = { } • temp1.ref = temp1.ref • temp1.offseted = true;
别名分析-例子 • Load temp1, auto1 • temp1.hold = {auto1} • temp1.ref = auto1.ref • temp1.offseted = auto.offseted • Load temp1, [temp2 + offset] • temp1.hold = {} • 由于MiniC中不存在指针数组,temp1的ref和offseted已经没有意义了
别名分析-例子 • Call function, arg temp1 • clear temp1.ref in all holds (and static var) • all offseted of temp1.ref set as true • Store temp1, auto1 • clear all {auto1} in holds • temp1.hold = {auto1} + temp1.hold • auto1.ref = temp1.ref • auto.offseted = temp1.offseted
别名分析的使用 • 所有的数据流分析都基于正确的别名分析信息 • 在分析指令行为时要充分考虑到别名信息 • 例子:到达定值 • Store可能对所有的引用定值
别名分析的使用:Load消除 • 对一个Load指令, 如果能确定其Load的目标,同时有一个临时变量hold了该目标,可以用Mov代替这个Load,后继的复写传播可以进一步消除Mov
ST_WORD TEMP_-1 [AUTO_0 + 0*0] ST_WORD TEMP_-2 [AUTO_1 + 0*0] LD_WORD TEMP_1 [AUTO_0 + 0*0] LD_WORD TEMP_0 [TEMP_1 + 0*0] ST_WORD TEMP_0 [AUTO_2 + 0*0] LD_WORD TEMP_3 [AUTO_1 + 0*0] LD_WORD TEMP_2 [TEMP_3 + 0*0] ST_WORD TEMP_2 [TEMP_1 + 0*0] LD_WORD TEMP_4 [AUTO_2 + 0*0] ST_WORD TEMP_4 [TEMP_3 + 0*0] RETURN ST_WORD TEMP_-1 [AUTO_0 + 0*0] ST_WORD TEMP_-2 [AUTO_1 + 0*0] MOV_ALL TEMP_1 TEMP_-1 LD_WORD TEMP_0 [TEMP_1 + 0*0] ST_WORD TEMP_0 [AUTO_2 + 0*0] MOV_ALL TEMP_3 TEMP_-2 LD_WORD TEMP_2 [TEMP_3 + 0*0] ST_WORD TEMP_2 [TEMP_1 + 0*0] MOV_ALL TEMP_4 TEMP_0 ST_WORD TEMP_4 [TEMP_3 + 0*0] RETURN
Store消除 • 对一个Store指令,将其看成一次define。如果该define的所有可达使用都存在别的变量hold其define的变量,可以删除该Store
ST_WORD TEMP_-1 [AUTO_0 + 0*0] ST_WORD TEMP_-2 [AUTO_1 + 0*0] MOV_ALL TEMP_1 TEMP_-1 LD_WORD TEMP_0 [TEMP_1 + 0*0] ST_WORD TEMP_0 [AUTO_2 + 0*0] MOV_ALL TEMP_3 TEMP_-2 LD_WORD TEMP_2 [TEMP_3 + 0*0] ST_WORD TEMP_2 [TEMP_1 + 0*0] MOV_ALL TEMP_4 TEMP_0 ST_WORD TEMP_4 [TEMP_3 + 0*0] RETURN MOV_ALL TEMP_1 TEMP_-1 LD_WORD TEMP_0 [TEMP_1 + 0*0] MOV_ALL TEMP_3 TEMP_-2 LD_WORD TEMP_2 [TEMP_3 + 0*0] ST_WORD TEMP_2 [TEMP_1 + 0*0] MOV_ALL TEMP_4 TEMP_0 ST_WORD TEMP_4 [TEMP_3 + 0*0] RETURN
复写传播之后 void swap(int* a, int* b) { int tmp; tmp = *a; *a = *b; *b = tmp; } LD_WORD TEMP_0 [TEMP_-1 + 0*0] LD_WORD TEMP_2 [TEMP_-2 + 0*0] ST_WORD TEMP_2 [TEMP_-1 + 0*0] ST_WORD TEMP_0 [TEMP_-2 + 0*0] RETURN
完成内容 • 因为初期的目标是完成一个供编译实习使用的Unicore32模拟器,所以重点是保证Unicore32模拟器的功能集,没有采用流水线结构,实现了下面几个功能: • Unicore32功能集; • 简单的Debug模式; • Unicore32运行周期的统计; • Cache;
流程 • 通过运行参数获得运行的文件以及模式; • 初始化内存寄存器,读入运行文件(ELF)加载到内存中; • 通过读入ELF文件的信息,运行代码,其中栈段从0x0c000000开始;同时进行系统性能和Cache的统计。 • 程序运行结束之后,输出运行时统计的信息
Unicore32功能集 • 这部分主要完成了Unicore32手册上所给的所有指令: • 其中ALU计算、跳转、读取等6个操作已经在中期报告中有详细说明。 • 处理LDM操作,即把多项寄存器的内容压入栈中:因为手册中没有给运行参数,所以在编译时进行了处理优化
Unicore32功能集 • 系统调用处理:完成了基本的I/O处理,及输入输出整型,字符型,字符串6个功能。 • 因为在指令手册中只告诉了系统调用的进入参数,具体功能参数并没有告诉。 • 具体参数是从0x50开始,一直到0x55:分别是:读入整型,读入字符、读入字符串,输出整型,输出字符,输出字符串。 • 其中r0保存的参数为:读入操作是需要保存的内存地址,输出整型、字符是输出的值,输出字符串是输出字符串在内存中的地址,以\0结束。
简单Debug模式 • 主要是进行调试,用来处理检查模拟器和编译出的汇编代码可能出现的错误 • 通过./SIM <> D来开启Debug模式。 • 每次运行完一条语句都会输出寄存器当前的信息。 • N:运行下一句。 • R:输出寄存器信息。 • C:输出状态寄存器信息4个值NZCV的信息。 • E:退出模拟器。 • S:关闭Debug模式
Unicore32运行的周期统计 • 根据5级流水,进行周期的粗略估计: • 处理的情况有:数据冒险和跳转冒险。 • 数据冒险:只有在出现前一条指令从内存中读入一个数,后一条指令就使用时; • 跳转冒险:只出现在条件跳转下; • 对于这两个冒险处理是加入一个气泡(总周期数加1)。
Cache • Cache基本信息:大小1024byte,高速缓存块大小32btye,高速缓存组数16组,采用两路组相联高速缓存,以及用最不常使用策略进行替换。 • 考虑到代码段和数据段:用I-Cache和D-Cache来进行分别的统计。 • Cache的加入主要是进行Cache命中率的分析,用来测试前面翻译成汇编代码的运行优化效率。 • CacheHit 和Miss以及Cache命中率统计情况。
最后总结 • 完成了Unicore32模拟器,并且能够跑出所有对所有的指令。 • 加入Debug模式,用来调试正确性。 • 加入了Cache和周期的统计,用来测试编译器优化的效率。 • 不足:运行时间过长,一旦运行指令过多时,所运行时间会很长。