关于NRV优化详细分析


 在C++中,函数返回整数或指针是通过eax寄存器进行传递的,理解起来比较简单。

    但是返回对象或结构体一直是令人感到困惑的问题。今天我整理了一下,将整个返回过程写下来,以作备用。

 

    还是先通过一个例子来理解这个问题:

首先,定义一个类Vector:

class Vector
{
public:
    int x,y;
};


然后定义函数add()对Vector对象进行操作:
Vector add(Vector& a, Vector & b)
{
    Vector v;
    v.x = a.x + b.x; 
    v.y = a.y + b.y;
    return v;
}

现在的问题是:

如果调用如下语句:

Vector a, b;
Vector c = add(a, b);  (调用拷贝构造函数,而不是赋值语句)

请问从a, b传入函数开始,一共创建了多少个对象?

 

在通常情况下我们会做出如下分析:

1. 在add()函数中创建对象v。

2. 函数返回,创建一个临时变量__temp0,并将v的值拷贝到__temp0中。

3. 最后创建对象c,通过操作符=,将__temp0中的对象拷贝到c中。


但其实,我们会在后面看到,整个过程就只创建了1个对象:c。

 

为了更清晰的分析整个调用过程,我们为Vector加上默认构造函数和拷贝构造函数,并增加一个静态变量count用于统计构造函数调用次数:

class Vector
{
public:
    static int count;
 
    static void init()
    {
        count = 0;
    }
 
    int x,y;
 
    Vector()
    {
        x = 0;
        y = 0;
         
        //For analysis.
        count++;
        printf("DefaultConstructor was called.[0x%08x]\n",this);
    }
 
    Vector(const Vector& ref)
    {
        x = ref.x;
        y = ref.y;
 
        //For analysis.
        count++;
        printf("CopyConstructor was called.[copy from 0x%08x to 0x%08x].\n",&ref, this);
    }
 
};
 
int Vector::count = 0;

然后在main()函数中写上调用代码:

Vector a, b;
Vector::init();
printf("\n--Test add() --\n");
Vector c = add(a, b);
printf("----Constructors were called %d times. ----\n\n\n",Vector::count);

使用cl编译。

(注:Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86, Microsoft (R) Incremental Linker Version 10.00.40219.01)

完成后,运行程序,得到如下结果:

--
 Test add() --
Default
 Constructor was called.[0x0012fef8]
Copy
 Constructor was called.[copy from 0x0012fef8 to 0x0012ff60].
----
 Constructors were called 2 times.
 ----

 

由此可知,在没有优化的情况下,整个调用过程共创建了两个对象:

即:c__temp0.

 

整个调用过程伪代码如下:

首先add()函数被编译器看做:

void add(Vector& __result, Vector& a, Vector & b)
{
    __result.x = a.x + b.x;
    __result.y = a.y + b.y;
    return;
}
而调用代码同时被修改为

Vector a, b;
Vector::init();
printf("\n--Test add() --\n");
Vector __temp0;     //构造函数.
add(__temp0,a, b);
Vector c(__temp0);  //拷贝构造函数.
printf("----Constructors were called %d times. ----\n\n\n", Vector::count);

现在就可以理解输出结果了吧。

这里要强调一点,看到”=”并不等于调用了Operator=()的代码,以下三种情况其实是等效的,都只调用了拷贝构造函数:

Vector b(a);
Vector b = a;
Vector b = Vector(a);

最精彩的部分在于,如果你用

cl
/Ox

编译代码,使优化达到最大,再次运行,得到如下结果:

--
 Test add() --
Default
 Constructor was called.[0x0012ff74]
----
 Constructors were called 1 times.
 ----

这次,只调用了默认构造函数。这样的修改被称作Named Return Value(NRV) Optimization。

什么是NRV优化呢,顾名思义,就是保存返回值的变量不再使用没名没姓的__temp0这样的东西了,而是直接把c作为返回变量,因此应该将NRV翻译为“有名字的返回变量”吧,侯捷翻译的《深入探索C++对象模型》居然把它称为“具名数值”,真是不知所云。

言归正传,NVR优化的伪代码如下:

Vector c;
add(c,a, b);
NVR优化的最大好处就是不会再去调用那次多余拷贝构造函数了(把__temp0拷贝到c) ,因此《深入探索C++对象模型》67页最下面才会说第一版没有拷贝构造函数,所以不能进行优化。其实是指优化的意义不大,或者说没有什么可优化的。
这是经过优化后的add函数汇编代码:
//
 segment of function: Vector add(Vecotr &a, Vector &b)
0040117B 
 mov         edx,dword ptr [esp+1Ch] //edx = b.x
0040117F 
 add         edx,dword ptr [esp+14h] //edx = edx + a.x
00401183 
 mov         ecx,dword ptr [esp+20h] //ecx = b.y
00401187 
 add         ecx,dword ptr [esp+18h] //ecx = ecx + a.y
0040118B 
 mov         dword ptr [esp+24h],edx //c.x = edx
0040118F 
 mov         edx,dword ptr ds:[40BDC0h]    //inline - printf arguments
00401195 
 push        edx                           //still arguments
00401196 
 push        4081E8h                       //arguments
0040119B 
 mov         dword ptr [esp+30h],ecx //c.y = ecx
0040119F 
 call        004011AE                      //call printf()
004011A4 
 add         esp,10h //function return routine.
004011A7 
 xor         eax,eax 
004011A9 
 pop         esi  
004011AA 
 add         esp,28h 
004011AD 
 ret

(注意:我尝试把Vector的拷贝构造函数删掉,同样生成了上面这段代码(一个字节都没变),因此我推测,拷贝构造函数并不是触发NRV优化的条件了,Lippman的书可能有点过时了。)

但是这样带来的坏处是,如果你在拷贝构造函数里面放上与拷贝无关的代码,比如我放入的printf和count++,那么这些东西就不会被调用了,产生优化前后代码不一致问题。所以大家要在此注意一下。













评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值