VisualStudio优化了复制移动省

蝎子

为了能发文,标题中的复制/移动省略是Copy/MoveElision的硬翻译,请各位大大海涵。下文中我会同时使用这两种术语。

VisualStudio中Copy/MoveElision的变化

在VisualStudio版本17.4预览版3中,我们显著增加了适用于Copy/MoveElision情况的数量,并让用户能够更好地控制是否启用这些转换。

Copy/MoveElision是什么?

当C++函数中的return关键字后跟非内置类型的表达式时,执行该return语句会将表达式的结果复制到调用函数的返回槽(ReturnSlot)中。为此,将调用非内置类型的复制或移动构造函数。然后,作为退出函数的一部分,将调用函数局部变量的析构函数,可能包括return关键字后面的表达式中命名的任何变量。

C++规范允许编译器直接在调用函数的返回槽中构造返回的对象,从而省略作为返回的一部分执行的复制或移动构造函数。与大多数其他优化不同,这种转换允许对程序的输出产生可观察的影响–即复制或移动构造函数以及关联的析构函数可以少调用一次。

VisualStudio中的Copy/MoveElision

C++标准要求在将返回值初始化为return语句的一部分时(例如,当返回类型为Foo的函数返回返回Foo()时),编译器需要执行Copy/MoveElision。MicrosoftVisualC++编译器始终根据需要对返回语句执行Copy/MoveElision,而不管传递给编译器的标志如何。此行为保持不变。

在VisualStudio17.4预览版3中对可选Copy/MoveElision的更改

当返回的值为命名变量时,编译器可能会省略复制或移动,但不是必需的。C++标准仍要求为命名的返回变量定义复制或移动构造函数,即使编译器在所有情况下都省略了构造函数。在VisualStudio版本17.4预览版3之前,当禁用优化(例如使用/Od编译器标志或使用了#pragmaoptimize(“”,off))时,编译器将仅执行强制Copy/MoveElision。使用/O2标志,编译器将通过简单的控制流为优化的函数执行可选的Copy/MoveElision。

从VisualStudio版本17.4预览版3开始,我们为开发人员提供了与新的/Zc:nrvo编译器标志保持一致的选项。默认情况下,当使用/O2标志、/permissive-编译代码时,或者在为/std:c++20或更高版本进行编译时,将传递/Zc:nrvo标志。通过此标志后,将尽可能执行复制和移动省略。我们希望在将来的版本中默认启用/Zc:nrvo。另外,开发者还可以使用/Zc:nrvo-标志显式禁用可选的Copy/MoveElision。请注意,无法禁用强制型的Copy/MoveElision。

在VisualStudio版本17.4预览版3中,当使用/Zc:nrvo、/O2、/permissive-或/std:c++20或更高版本的标志启用可选复制/移动省略时,我们还增加了Copy/MoveElision的位置。

可选Copy/MoveElision的示例

可选Copy/MoveElision的最简单示例是以下函数:FooSimpleReturn(){Fooresult;returnresult;}

在这种情况下,如果传递了/O2标志,则早期版本的MSVC编译器已将结果的复制或移动到返回槽中。在VisualStudio版本17.4预览版3中,如果传递了/permissive-、/std:c++20或更高版本或/Zc:nrvo标志,也会省略复制或移动,如果传递了/Zc:nrvo-标志,则保留复制或移动。

从VisualStudio版本17.4预览版3开始,如果将/O2、/permissive-、/std:c++20或更高版本或/Zc:nrvo标志传递给编译器,而/Zc:nrvo-标志未传递到编译器,我们现在在以下其他情况下执行复制/移动省略。

在循环中返回

FooReturnInALoop(intiterations){for(inti=0;iiterations;++i){Fooresult;if(i==(iterations/2)){returnresult;}}}结果对象将在循环的每次迭代开始时正确构造,并在每次迭代结束时销毁。在返回结果的迭代中,退出函数时不会调用其析构函数。当返回的对象超出该函数的范围时,函数的调用方将销毁该对象。

在异常处理中返回

FooReturnInTryCatch(){try{Fooresult;returnresult;}catch(…){}}如果传递了/O2、/permissive-、/std:c++20或更高版本,或者传递了/Zc:nrvo标志,而/Zc:nrvo-标志未传递,则结果对象的复制或移动现在将被省略。我们现在还可以妥善处理更复杂的情况,例如:

intn;

voidthrowFirstThreeIterations(){++n;if(n=3)thrown;}

FooComplexTryCatch(){Label1:Fooresult;

try{throwFirstThreeIterations();returnresult;}catch(…){gotoLabel1;}}

结果对象将在调用方函数的返回槽中构造,并且在成功返回时不会为其调用复制/移动构造函数或析构函数。引发异常时,是否析构结果对象取决于向编译器传递哪些异常处理标志。默认情况下,不会发生堆栈展开,因此不会调用析构函数。但是,如果使用/EHs、/EHa或/EHr标志启用了堆栈展开异常处理,则gotoLabel1将导致调用结果的析构函数,因为它跳转到初始化结果之前。无论哪种方式,当再次到达表达式Foo结果时,将在返回槽中再次构造对象。

复制具有默认参数的构造函数

现在,我们可以正确检测到具有默认参数的复制或移动构造函数仍然是复制或移动构造函数,因此可以在上述情况下被省略。具有默认参数的复制构造函数如下所示:structStructWithCopyConstructorDefaultParam{intX;

StructWithCopyConstructorDefaultParam(intx):X(x){}StructWithCopyConstructorDefaultParam(StructWithCopyConstructorDefaultParamconstoriginal,intdefaultParam=0):X(original.X+defaultParam){printf(“Copyconstructorcalled.\n”);}};

对NRVO的限制

尽管MSVC编译器现在在更多情况下执行Copy/MoveElision,但并不总是能够执行它。若要了解为什么会这样,请考虑以下函数:FooWhichShouldIReturn(boolcondition){FooresultA;if(condition){FooresultB;returnresultB;}returnresultA;}

复制省略构造要在返回槽中返回的对象,但在这种情况下,应在返回槽中构造哪个对象?为了在返回结果A时省略结果A的副本,必须在返回槽中构造它。但是,如果条件为真,则需要在销毁结果A之前在返回槽中构造结果B。无法对两个路径执行复制省略。

我们目前选择避免在函数中的所有路径上执行可选的Copy/MoveElision,如果在任何路径上它是不可能的的话。但是,对内联决策、死代码消除和其他优化的更改可能会更改Copy/MoveElision的可能性。因此,编写依赖于命名变量的Copy/MoveElision的某些行为的代码是不安全的,除非使用/Zc:nrvo-禁用了所有可选的Copy/MoveElision。

只要启用了堆栈展开异常处理或未引发异常,仍然可以安全地假定每个构造函数调用都有匹配的析构函数调用。

总结

写着旧时代的C++,一直都为如何高性能地返回一个对象发愁。没错,正是在下。

最后

MicrosoftVisualC++团队的博客是我非常喜欢的博客之一,里面有很多关于VisualC++的知识和最新开发进展。大浪淘沙,如果你对VisualC++这门古老的技术还是那么感兴趣,则可以经常去他们那(或者我这)逛逛。本文来自:《ImprovingCopyandMoveElision》




转载请注明:http://www.aierlanlan.com/rzdk/5926.html