首先要明确两个概念
- 函数实参的入栈从右向左
- 栈区从高地址向低地址偏移
接下来看下面一段代码
void fun(int a,int b,int c){
std::cout<<&a<<" "<<&b<<" "<<&c<<std::endl;
}
int main(){
fun(1,2,3);
}
这段代码看起来很简单但是你在不同环节下运行起来可能就会有问题了windows下vs
linux下vscode
可以看出一个是从左向右递增,一个是从左向右递减,你觉得这个是为什么呢?
我也不卖关子了,出现这种情况的原因是不同编译器将帧地址传递给函数后对函数局部变量的处理顺序不同。
函数从调用开始到结束过程
创建栈帧
在编译期编译器会根据类中局部变量的使用情况确定栈帧的大小
栈 |
---|
主函数帧指针 |
主函数内局部变量 |
… |
返回地址 |
函数的帧指针(指向主函数帧指针) |
函数的局部变量 |
… |
栈指针 |
以上就是一个主函数调用一个函数时栈内的情况,栈帧可以理解为一个函数的活动空间,或者说运行环境,下面简述几个概念。
- 帧指针:函数的主心骨,局部变量的创建都是在它的地址基础上偏移,它内部存函数调用者的帧指针地址
- 栈指针,就是栈顶
- 返回地址,存储调用函数完成后需要返回到的下一条指令的地址
栈帧的大小是在编译期间确定的,形参也处于局部变量的范畴,将实参从调用者的栈帧内移动到被调用者的栈帧内就涉及到了几种传参方式
- 值传递
- 址传递
- 引用传递(也是通过址传递)
栈帧和代码区函数的交互
函数的运行在代码区,平时看汇编的时候如果你注意就会发现发生函数调用时一定会传一个东西,这个东西就是帧指针,比如下面这一坨
#include<iostream>
using namespace std;
void fun(){
int nums[1024];
}
int main() {
fun();
}
fun():
push rbp
mov rbp, rsp
sub rsp, 3976
nop
leave
ret
main:
push rbp
mov rbp, rsp
call fun()
mov eax, 0
pop rbp
ret
这个rbp寄存器里面存的就是这个帧指针,sub rsp, 3976
这个条指令就是进行栈指针的偏移,这个3976是栈帧内为局部变量预留的空间,至此可以说栈帧的创建完成了,接下来该使用了,局部变量的创建可以看到都是通过在rbp的基础上进行偏移的,需要注意的是编译器会进行各种优化你需要编译时加上-0o关闭优化,还有就是栈的对齐方式是16字节。
所以说当面试官问你栈区有没有产生内存碎片的可能时,你可以从容的回答栈的对齐大多数采用的是16字节可能会因为对齐方式而产生比较多的内存碎片,这些碎片显然是用不了的,高性能的代价往往是高内存消耗。
返回值的处理
一般来说对于返回值是函数的调用者需要关注的东西,现在的大部分编译器返回值的传递其实都是通过寄存器完成的,在不进行各种优化的情况下编译器会根据返回值的数据进行选择,如果数据量较大就使用寄存器返回地址,如果数据量较小则直接使用寄存器将结果返回,寄存器返回后会调用者的栈帧内拷贝一份函数的返回值,这个过程发生在函数栈帧被释放前,这个过程结合我之前的一篇文章:C++:完美转发和移动语义看会比较清楚。
上面那个过程显然不太合理,首先返回值不是被调用者需要考虑的东西,如果说我们直接在函数调用前在调用者的栈帧内创建好被调用者的返回值然后使用寄存器将这个地址传过去,在这个地址的基础上函数再进行处理不就行了吗?实际上这个就是RVO优化的原理,此外RVO还会判断返回值有没有被用到,如果没用的话也是会优化掉的。
RVO发生的条件通常是:在函数中创建了一个局部对象,并将其作为函数返回值,而调用方直接使用了这个返回值。在这种情况下,编译器可以直接将该局部对象放置到调用方期望的位置,而不需要进行复制或移动操作。
RVO失效情景
- 返回多个对象: 如果函数返回多个对象,而不是一个单独的临时对象,那么RVO优化可能不适用。RVO通常仅适用于返回单个对象的情况
return pair<pair<int,int>,pair<int,int>>();
- 虚函数调用: 当函数是虚函数时,编译器可能无法确定函数返回的对象的确切类型,从而阻止了RVO优化的应用
struct A{virtual A fun(){return *this;}};
struct B:public A{virtual B* fun(){return *this;}};
A* b=new B;
b->fun();
- 复杂的返回逻辑
if(a) return a;
else if(b) return b;
return c;
观测RVO优化的过程,直接从上下文地址的变化非常直观
A get(){
A a;
cout<<&a<<endl;
return a;
}
int main() {
A a;
cout<<&a<<endl;
a=get();
A b;
cout<<&b<<endl;
}
可以看到当发生RVO优化时会在调用者的栈帧内临时申请一块空间,生命周期过了就被下一个变量覆盖了,现在你应该对RVO和临时对象有了新的认识
总的来说,移动语义的引入可以说从根源上缓解了临时对象对程序性能的影响,就业务中可能出现的各种情况编译器可能不能很好的进行各种优化,所以合适的代码书写还是很有必要的,不要寄希望于各种优化了
栈帧的释放
返回值处理完成后,首先帧指针跳转到上一个节点然后栈指针向高地址偏移,直到遇到返回地址程序接着运行
回归到开头时候的问题,现在你大概已经直到答案了,没错就是不同操作系统在不同编译器下函数对参数的处理顺序不同造成的。