摘要:说明 Visual C++ 编译器如何消除各种情况下多余的 Copy 构造函数调用和析构函数调用。
Microsoft 一直在为 Visual C++ 优化编译器寻找新的技术和优化方法,以便尽可能地为编程人员提供更高的性能。本文将说明编译器如何尝试在各种情况下消除多余的 Copy 构造函数调用和析构函数调用。
通常,当一个方法返回对象的一个实例时,将创建一个临时对象,并通过复制构造函数将其复制到目标对象。C++ 标准允许省略复制构造函数的一部分(即使这样做会导致不同的程序行为),这对编译器将这两个对象作为一个对象进行处理具有副作用。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 constructor
I am in constructor
I am in copy constructor
I am in destructor
I am in destructor
I am in destructor
若启用 NRVO (cl /O2 sample1.cpp),预期输出将是:
I am in constructor
I am in constructor
I am in destructor
I 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 constructor
A: I am in constructor
B: I am in constructor
A: I am in copy constructor
B: I am in destructor
A: I am in destructor
A: I am in copy constructor
A: I am in destructor
A: I am in destructor
A: I am in destructor
而加入 NRVO 优化 (cl /O2 sample2.cpp) 后,输出将变成:
A: I am in constructor
A: I am in constructor
B: I am in constructor
A: I am in copy constructor
B: I am in destructor
A: I am in destructor
A: I am in destructor
A: 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 constructor
I am in constructor
I am in destructor
I caught the exception
I am in destructor
若去掉“throw”的注释说明,输出将变成:
I am in constructor
I am in constructor
I am in copy constructor
I am in destructor
I am in destructor
I am in destructor
现在,若去掉“throw”的注释说明,并触发 NRVO,输出将如下所示:
I am in constructor
I am in constructor
I am in destructor
I 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 constructor
I am in constructor
I 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 constructor
I 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 constructor
I am in constructor
I am in copy constructor
I am in destructor
I am in destructor
I 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 4
RVO 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 constructor
I am in constructor
I am in copy constructor
I am in destructor
I am in destructor
I 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 所示),可能会导致应用程序崩溃。
Microsoft 一直在为 Visual C++ 优化编译器寻找新的技术和优化方法,以便尽可能地为编程人员提供更高的性能。本文将说明编译器如何尝试在各种情况下消除多余的 Copy 构造函数调用和析构函数调用。
通常,当一个方法返回对象的一个实例时,将创建一个临时对象,并通过复制构造函数将其复制到目标对象。C++ 标准允许省略复制构造函数的一部分(即使这样做会导致不同的程序行为),这对编译器将这两个对象作为一个对象进行处理具有副作用。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 constructor
I am in constructor
I am in copy constructor
I am in destructor
I am in destructor
I am in destructor
若启用 NRVO (cl /O2 sample1.cpp),预期输出将是:
I am in constructor
I am in constructor
I am in destructor
I 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 constructor
A: I am in constructor
B: I am in constructor
A: I am in copy constructor
B: I am in destructor
A: I am in destructor
A: I am in copy constructor
A: I am in destructor
A: I am in destructor
A: I am in destructor
而加入 NRVO 优化 (cl /O2 sample2.cpp) 后,输出将变成:
A: I am in constructor
A: I am in constructor
B: I am in constructor
A: I am in copy constructor
B: I am in destructor
A: I am in destructor
A: I am in destructor
A: 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 constructor
I am in constructor
I am in destructor
I caught the exception
I am in destructor
若去掉“throw”的注释说明,输出将变成:
I am in constructor
I am in constructor
I am in copy constructor
I am in destructor
I am in destructor
I am in destructor
现在,若去掉“throw”的注释说明,并触发 NRVO,输出将如下所示:
I am in constructor
I am in constructor
I am in destructor
I 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 constructor
I am in constructor
I 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 constructor
I 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 constructor
I am in constructor
I am in copy constructor
I am in destructor
I am in destructor
I 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 4
RVO 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 constructor
I am in constructor
I am in copy constructor
I am in destructor
I am in destructor
I 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 所示),可能会导致应用程序崩溃。