Visual C++ 2005 中的命名返回值优化

Visual C++ 2005 中的命名返回值优化
整理:Ackarlix

Ayman B. Shoukry
Visual C++ Compiler Team

摘要:说明 Visual C++ 编译器如何消除各种情况下多余的 Copy 构造函数调用和析构函数调用。

Microsoft 一直在为 Visual C++ 优化编译器寻找新的技术和优化方法,以便尽可能地为编程人员提供更高的性能。本文将说明编译器如何尝试在各种情况下消除多余的 Copy 构造函数调用和析构函数调用。

通常,当一个方法返回对象的一个实例时,将创建一个临时对象,并通过复制构造函数将其复制到目标对象。C++ 标准允许省略复制构造函数的一部分(即使这样做会导致不同的程序行为),这对编译器将这两个对象作为一个对象进行处理具有副作用 请参阅 12.8 节 Copying class objects 的第 15 段;请参阅 HYPERLINK /l "_参考资料" 参考资料)。Visual C++ 8.0 编译器充分利用了标准提供的灵活性,并添加了一个新功能:命名返回值优化(Named Return Value Optimization,NRVO)。NRVO 消除了复制构造函数和析构函数基于堆栈的返回值。这样进行优化,可以去掉多余的复制构造函数调用和析构函数调用,从而提高整体性能。注意,这会导致优化的程序和未优化的程序之间的不同行为(请参阅优化的副作用部分)。

在某些情况下不会发生优化(请参阅优化限制部分中的示例)。比较常见的情况有:

  • 返回不同命名对象的不同路径。

  • 引入 EH 状态的多个返回路径(即使所有路径上返回的都是同一个命名对象)。

  • 在内联 asm 块内引用了返回的命名对象。

优化描述

图 1 中的一个简单示例说明了这种优化及其实现过程:

A MyMethod (B &var){   A retVal;   retVal.member = var.value + bar(var);   return retVal;}

1. 原始代码

使用以上函数的程序可能具有以下结构:

valA = MyMethod(valB);

MyMethod 返回的这个值是在 ValA 使用的隐藏参数所指向的内存空间中创建的。当我们公开该隐藏参数,并显式显示构造函数和析构函数时,图 1 中的函数将如下所示:

A MyMethod (A &_hiddenArg, B &var){   A retVal;   retVal.A::A(); // constructor for retVal   retVal.member = var.value + bar(var);   _hiddenArg.A::A(retVal);  // the copy constructor for A   return;retVal.A::~A();  // destructor for retVal}

2. 没有 NRVO 的隐藏参数代码(伪代码)

您可能会从以上代码中发现一些可以优化的地方。基本思想是,消除基于堆栈的临时值 (retVal),并使用隐藏参数。因此,这将消除基于堆栈的值的复制构造函数和析构函数。以下是基于 NRVO 的优化代码:

A MyMethod(A &_hiddenArg, B &var){   _hiddenArg.A::A();   _hiddenArg.member = var.value + bar(var);   Return}

3. 有 NRVO 的隐藏参数代码(伪代码)

代码示例

Sample 1:简单示例

#include class RVO{public:                   RVO(){printf("I am in constructor/n");}            RVO (const RVO& c_RVO) {printf ("I am in copy constructor/n");}            ~RVO(){printf ("I am in destructor/n");}            int mem_var;       };RVO MyMethod (int i){            RVO rvo;            rvo.mem_var = i;            return (rvo);}int main(){            RVO rvo;            rvo=MyMethod(5);}

4. Sample1.cpp

编译 sample1.cpp 时启用以及不启用 NRVO 将产生不同的行为。

若不启用 NRVO (cl /Od sample1.cpp),预期输出将是:

I am in constructorI am in constructorI am in copy constructorI am in destructorI am in destructorI am in destructor

若启用 NRVO (cl /O2 sample1.cpp),预期输出将是:

I am in constructorI am in constructorI am in destructorI am in destructor

Sample 2:比较复杂的示例

#include class A {  public:    A() {printf ("A: I am in constructor/n");i = 1;}    ~A() { printf ("A: I am in destructor/n"); i = 0;}    A(const A& a) {printf ("A: I am in copy constructor/n"); i = a.i;}    int i, x, w;}; class B {  public:    A a;    B()  { printf ("B: I am in constructor/n");}    ~B() { printf ("B: I am in destructor/n");}    B(const B& b) { printf ("B: I am in copy constructor/n");}};A MyMethod(){    B* b = new B();    A a = b->a;    delete b;    return (a);}int main(){    A a;    a = MyMethod();}

5. Sample2.cpp

不启用 NRVO (cl /Od sample2.cpp) 的输出将如下所示:

A: I am in constructorA: I am in constructorB: I am in constructorA: I am in copy constructorB: I am in destructorA: I am in destructorA: I am in copy constructorA: I am in destructorA: I am in destructorA: I am in destructor

而加入 NRVO 优化 (cl /O2 sample2.cpp) 后,输出将变成:

A: I am in constructorA: I am in constructorB: I am in constructorA: I am in copy constructorB: I am in destructorA: I am in destructorA: I am in destructorA: I am in destructor

优化限制

某些情况下,不会加入优化。以下就是这种限制的几个示例。

Sample 3:异常示例

遇到异常时,隐藏参数必须在它所替换的临时(对象)范围内进行析构。以下示例进行说明:

//RVO class is defined above in figure 4#include RVO MyMethod (int i){            RVO rvo;            rvo.mem_var = i;            throw "I am throwing an exception!";            return (rvo);}int main(){            RVO rvo;            try             {                        rvo=MyMethod(5);            }            catch (char* str)            {                        printf ("I caught the exception/n");            }}

6. Sample3.cpp

若没有启用 NRVO (cl /Od /EHsc sample3.cpp),预期输出将是:

I am in constructorI am in constructorI am in destructorI caught the exceptionI am in destructor

若去掉“throw”的注释说明,输出将变成:

I am in constructorI am in constructorI am in copy constructorI am in destructorI am in destructorI am in destructor

现在,若去掉“throw”的注释说明,并触发 NRVO,输出将如下所示:

I am in constructorI am in constructorI am in destructorI am in destructor

也就是说,图 6 所示的 sample3.cpp 在使用和未使用 NRVO 的情况下具有相同的行为。

Sample 4:不同命名对象的示例

要充分利用优化,所有退出路径必须返回同一个命名对象。为了说明这一点,请考虑 sample4.cpp:

#include class RVO{public:                   RVO(){printf("I am in constructor/n");}            RVO (const RVO& c_RVO) {printf ("I am in copy constructor/n");}            int mem_var;       };RVO MyMethod (int i){            RVO rvo;            rvo.mem_var = i;      if (rvo.mem_var == 10)         return (RVO());            return (rvo); }int main(){            RVO rvo;            rvo=MyMethod(5);}

7. Sample4.cpp

启用优化后的输出 (cl /O2 sample4.cpp) 与未启用任何优化的输出 (cl /Od sample.cpp) 相同。这是因为并非所有返回路径都返回同一个命名对象,所以实际上没有发生 NRVO。

I am in constructorI am in constructorI am in copy constructor

如果将以上示例更改为在所有退出路径中返回 rvo(如 图 8. Sample4_modified.cpp 所示),那么优化将消除复制构造函数:

#include class RVO{public:                   RVO(){printf("I am in constructor/n");}            RVO (const RVO& c_RVO) {printf ("I am in copy constructor/n");}            int mem_var;       };RVO MyMethod (int i){            RVO rvo;           if (i==10)         return (rvo);      rvo.mem_var = i;            return (rvo); }int main(){            RVO rvo;            rvo=MyMethod(5);}

8. 修改 Sample4_Modified.cpp 以利用 NRVO

输出 (cl /O2 Sample4_Modified.cpp) 将如下所示:

I am in constructorI am in constructor

Sample 5:EH 限制示例

下面的图 9 展示与图 8 相同的示例,但向 RVO 类中加入了析构函数。拥有多个返回路径并引入这样一个析构函数将在函数中创建 EH 状态。由于编译器跟踪机制的复杂性,它在跟踪需要析构的对象时,会回避返回值优化。事实上,这是 Visual C++ 2005 需要在未来改进的地方。

//RVO class is defined above in figure 4#include RVO MyMethod (int i){            RVO rvo;           if (i==10)         return (rvo);      rvo.mem_var = i;            return (rvo); }int main(){            RVO rvo;            rvo=MyMethod(5);}

8. Sample5.cpp

编译 Sample5.cpp 时使用和不使用优化将产生相同的结果:

I am in constructorI am in constructorI am in copy constructorI am in destructorI am in destructorI am in destructor

为了充分利用 NRVO,请将 MyMethod 更改为以下代码,以尝试消除这种情况下的多个返回点:

RVO MyMethod (int i){            RVO rvo;      if (i!=10)         rvo.mem_var = i;      return(rvo);  }

Sample 6:内联 asm 限制

编译器回避执行 NRVO 的另一个情况是,在内联 asm 块中引用了命名返回对象。为了说明这一点,请考虑以下示例:

#include //RVO class is defined above in figure 4RVO MyMethod (int i){            RVO rvo;__asm {      mov eax,rvo   //comment this line out for RVO to kick in      mov rvo,eax //comment this line out for RVO to kick in          }            return (rvo); }int main(){            RVO rvo;            rvo=MyMethod(5);}

9. sample6.cpp

编译 sample6.cpp 时启用优化 (cl /O2 sample6.cpp) 仍然无法利用 NRVO。这是因为,实际上在内联 asm 块中引用了返回的对象。因此,无论是否使用了优化,输出都将如下所示:

I am in constructorI am in constructorI am in copy constructorI am in destructorI am in destructorI am in destructor

从该输出中我们不难发现,并没有消除复制构造函数调用和析构函数调用。如果去掉 asm 块的注释说明,就会消除这些调用。

优化的副作用

编程人员应该认识到,这样的优化可能会影响应用程序的流程。以下示例说明这一副作用:

#include int NumConsCalls=0;int NumCpyConsCalls=0;class RVO{public:                   RVO(){NumConsCalls++;}            RVO (const RVO& c_RVO) {NumCpyConsCalls++;}};RVO MyMethod (){            RVO rvo;            return (rvo); }void main(){           RVO rvo;           rvo=MyMethod();       int Division = NumConsCalls / NumCpyConsCalls;       printf ("Constructor calls / Copy constructor calls = %d/n",Division);}

10. sample7.cpp

编译 sample7.cpp 时不启用优化 (cl /Od sample7.cpp) 将产生大多数用户预期的结果。构造函数调用了两次,而复制构造函数调用了一次,因此除法运算 (2/1) 的结果为 2。

Constructor calls / Copy constructor calls = 2

从另一方面来看,如果编译以上代码时启用了优化 (cl /O2 sample7.cpp),则将加入 NRVO,因而会消除复制构造函数调用。因此,NumCpyConsCalls 将为 ZERO,导致除法出现 ZERO 异常,如果该异常没有正确处理(如 sample7.cpp 所示),可能会导致应用程序崩溃。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值