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 所示),可能会导致应用程序崩溃。