日记大全

日记大全 > 句子大全

编译器对内联函数的处理(C语言实现)

句子大全 2023-09-08 04:06:01
相关推荐

上一篇简单说了一下编译器的后端模块,这里给一个内联函数模块的代码。

编译器后端的实现,C语言

内联函数,是C/C++语言为了节约函数调用的开销,而设计的一个关键字。

简单而常用的功能写成内联函数,编译器就会把该函数的代码在调用处展开,而不产生实际的函数调用。

一般情况下,内联函数的代码都比较少

如果函数比较大,即使声明了内联,一般编译器也不会做内联。

上图是内联函数的optimizer结构,和它的函数指针。

它会遍历需要编译的所有函数,查找调用了某个内联函数的地方,并进行内联处理,具体在_optimize_inline2()函数里。

之前的文章提到过,编译器是把程序分成函数基本块三地址码三个层次。

编译器后端对代码的表示层级

要检查一个函数内的内联函数调用,也要按照这个层次来检查。

函数调用,是一条三地址码。

先检查每一个基本块,如果它含有函数调用,在划分基本块时就提前给它设置一个call_flag标志,以后就不必检查它的每条三地址码了。

没有call_flag的基本块,直接跳过。

如果因为内联函数而做过基本块的拆分的,需要把三地址码从原来的块移过来。

例如,

inline add(int a, int b) {return a + b;}

int main() {

int i = 1;

int j = 2;

int k = add(i, j); // 在这里展开内联函数

k += 1;

return 0;

}

我们并不关注内联函数add的内部怎么实现的(它有可能有多个基本块),只需要把它的所有基本块复制一份,然后放到j = 2和k += 1之间。

本来,j = 2和k += 1在同一个基本块内,添加上内联函数的代码之后,k += 1就要放在内联函数的最后一个基本块内,与j = 2不在同一基本块了。

386-391行,是处理这种情况的。

在移动三地址码之前,必须先把它从链表上取下来。在把它取下来之前,必须把链表指针l2移动到下一个位置,否则链表就乱了(383-384-387行)。

如果三地址码不是函数调用,它肯定不是内联函数的调用,继续。

如果是函数调用,被调函数是它的第一个源操作数(0号索引)。

被调函数符合以下几个条件时才可以内联:

1,它不能是函数指针的调用,必须是直接的函数调用。

因为函数指针的值可以被修改,这次调用的是这个函数,下次就可能是那个,所以肯定不能把它内联。

2,它必须是在源码里定义的函数,不能是只有个头文件声明的库函数

库函数拿不到源码和三地址码,也就没法内联了。

如果在汇编层面内联,那要延后到机器码生成时才可以,在这里是没法内联的。

3,要声明了内联,并且不能含有可变参数

可变参数的处理,与函数调用的内存布局密切相关。

一旦内联,可变参数要怎么处理?

不好处理,所以printf()这类的函数,不内联。

4,内联函数的代码量要少,这里以10个基本块作为上限,超过10个基本块的就不内联了。

三地址码的条数作为上限,要更精确一些。但之前没有统计这个数据,只统计了基本块的个数。

内联之后,这个基本块的call_flag就可以清零了。

bb_cur的地址同时作为输入参数和输出参数,输入当前基本块,返回(复制的)内联函数的最后一个基本块

421行,调用_do_inline(),处理内联。

f是调用函数,f2是被调用的内联函数,c是调用内联函数的三地址码,ast是语法树的指针。

425-427,内联之后,这条调用代码就没作用了,释放内存。

bb2是处理内联之前的基本块,bb_cur(输入和输出参数)有可能被改变。

bb2的call_flag取决于它的其他三地址码,如果它还有无法内联的函数调用的话。

bb2的后续代码,将移动到bb_cur里。

所以如果bb2有return语句,ret_flag被设置,那么这个标志就传递给了bb_cur。

如果一个基本块含有return语句,那么return一定是这个基本块的最后一条语句(因为接下来函数就返回了)。

return肯定不是函数调用,所以如果内联函数调用return语句在同一个基本块,那么在内联处理之后,return就会被转移到(复制的)内联函数的最后一个基本块。

230行,_copy_codes(),复制内联函数的代码,同时获取它的形参列表。

不一定每个形参都会使用。如果形参被使用了,那么它一定有AST节点,把这个节点存到数组argv里,每个形参存它第一次被使用时的AST节点。

要把实参赋值给形参,即生成一条赋值指令,放在内联函数展开位置的前面。

不能直接使用实参,因为实参和形参不是同一个局部变量。要防备程序员把代码写成这样:int add(int a, int b) { a += b; return a; }

这么写也是合乎语法的C代码。

但如果直接使用实参,内联之后实参的值就被形参a的+=运算给改变了,这显然是错误的内联。

所以,要先给形参生成赋值语句。如果后续实参确实不使用,在基本块的优化时再把它优化掉。

内联之后的代码,如下:

int i = 1, j = 2;

int a = i, b = j;

int k = a + b; k+= 1; return 0;

像i = 1; a = i; 这类代码,在生成DAG图时可以做优化:

检查i的父节点是不是=,如果是=,那么就让a = 1,而不是a = i。

这样代码就变成了i = 1和a = 1,并且之后的运算只使用a和b和k,不使用i和j,所以i = 1和j = 2这两条无效语句可以被优化掉。

搜索内联函数f2的局部变量,使用图的宽度优先搜索。变量是有作用域的,局部变量可以声明在函数的任何一个代码块里。

内联函数的局部变量,包括它的形参,都要添加成函数caller的局部变量。

复制的内联函数的基本块,都要添加到主调函数的展开位置。

基本块之间的前后关系,也要重新设置。

内联函数的最后一个基本块,在复制展开之后,也不再是(主调函数的)最后一个。

内联函数里的跳转指令,也要修改目的地址。

复制内联函数_copy_codes()函数:

就是复制每一个基本块和里面的三地址码序列。

内联函数的return语句,要修改为对主调函数的、调用语句的、目的变量的、赋值语句。

复制完了基本块之后,基本块的关系和跳转的目的地,都要修正。

内联函数的处理,大概不到500行C代码,但是细节还是比较多了。

想了解更多精彩内容,快来关注闲聊代码

阅读剩余内容
网友评论
相关内容
拓展阅读
最近更新