前言
我们写的函数是怎么返回的,该如何返回一个临时变量,临时变量不是出栈就销毁了吗,为什么可以传递给调用方?返回对象的大小对使用的方式有影响吗?本文将带你探究这些问题,阅读本文需要对函数栈帧有一定的理解,并了解基本的汇编指令。
文章汇编代码:采用 GCC 8.3.1,对 C 代码使用 -Og 优化级别生成的可执行程序,再用 objdump -d 反汇编的结果。
寄存器保存
如果返回的对象比较小,寄存器可以放得下,返回值将被放到 %rax 中。%rax 只能存放整数数据和指针,浮点数使用另外一组单独的寄存器。
来看一段简单的代码:
// 一段简单让两数相乘的代码,写成这种形式,主要是尽量减少编译器优化
long mult2(long a, long b) {long t = a * b;return t;
}void mult_store(long x, long y, long* dest) {long t = mult2(x, y);*dest = t;
}
00000000004004d2 <mult2>:# a in %rdi,b in %rsi4004d2: mov %rdi,%rax # 把 a 移动到 %rax4004d5: imul %rsi,%rax # 此时 %rax 保存的是参数 a,再将 %rsi 保存的参数 b 乘到 %rax4004d9: retq # 这时 %rax 保存的是 a * b00000000004004da <mult_store>:# x in %rdi,y in %rsi,dest in %rdx4004da: push %rbx4004db: mov %rdx,%rbx # 保存 dest,采取调用方保存4004de: callq 4004d2 <mult2> # 调用 mult24004e3: mov %rax,(%rbx) # 将 %rax 保存的 a * b# 移动到 %rbx 保存的 dest 指针指向的内存处# t 并没有实际的作用,编译器将其创建优化掉了4004e6: pop %rbx4004e7: retq
编译器优化
上面所说的都是比较小的内置类型,那假如返回的对象很大,%rax 放不下该怎么办?
下面介绍一种 C++ 对返回值的优化方式,实际编译器并不一定会使用该方式。
class qgw {// 有默认构造函数、拷贝构造函数、析构函数等long a1;long a2;long a3;
} qgw;qgw fun() {qgw q;// 处理 qreturn q;
}
如果返回值很小,我们可以使用寄存器取到返回值,现在又该怎么办呢?Stroustrup 在 cfront 中的解决方案是一个双阶段优化:
- 加上一个额外参数,类型是 class object 的一个 reference
- 这个参数在最后用 “返回值” 构建
- 在 return 指令之前插入一个拷贝构造调用操作,以便将想要返回的 object 的内容当做上述新增参数的初值
上述代码经转化后如下:
void fun(qgw& __result) {qgw q;// 编译器产生的默认构造函数调用操作q.qgw::qgw();// 处理 q// 编译器产生的拷贝构造调用操作__result.qgw::qgw(q);return;
}
qgw qin = fun();
// 转化为
// 不必为 qin 调用默认构造函数
qgw qin;
fun(qin);// fun() 函数返回值调用 test 函数
fun().test();
// 转化为
// 编译器生成的临时变量
qgw __temp0;
(fun(__temp0), __temp0).test();
还有一种被称为 Named Return Value(NRL)优化,被视为标准 C++ 编译器的一个义不容辞的优化操作。
qgw fun() {qgw q;// 处理 qreturn q;
}// 直接优化为
void fun(qgw& __result) {// 默认构造被调用__result.gqw::qgw();// 直接处理 __resultreturn;
}
经上述处理后,函数没有真正意义上的返回值了,也就不需要处理大对象的情况了。
对于传参请参考:传参的理解